PCjs Machines

Home of the original IBM PC emulator for browsers.

Logo

Proficient C

The following document is from the Microsoft Programmer’s Library 1.3 CD-ROM.

Proficient C



The Microsoft(R) guide to intermediate and advanced C programming


                                                    by AUGIE HANSEN



PUBLISHED BY
Microsoft Press
A Division of Microsoft Corporation
16011 N.E. 36th Way, Box 97017, Redmond, Washington 98073-9717

Copyright (C) 1987 by Augie Hansen
All rights reserved. No part of the contents of this book may be reproduced
or transmitted in any form or by any means without the written permission
of the publisher.

Library of Congress Cataloging in Publication Data
Hansen, Augie
Proficient C.
Includes index.
1. C (Computer program language) I. Title.
QA76.73C15H367    1987    005.13'3    86-31109
ISBN 1-55615-007-5

Printed and bound in the United States of America.

        4 5 6 7 8 9 FGFG 8 9 0 9 8 7

Distributed to the book trade in the United States by Harper & Row.

Distributed to the book trade in Canada by General Publishing Company,
Ltd.

Distributed to the book trade outside the United States and Canada by
Penguin Books Ltd.

Penguin Books Ltd., Harmondsworth, Middlesex, England
Penguin Books Australia Ltd., Ringwood, Victoria, Australia
Penguin Books N.Z. Ltd., 182-190 Wairau Road, Auckland 10, New Zealand
British Cataloging in Publication Data available


Microsoft(R) and MS-DOS(R) are registered trademarks of Microsoft
Corporation.



This book is dedicated to my parents, who worried throughout my childhood
and teenage years that I would electrocute myself in my electronics
laboratory, and to my family, Doris, Lindsey, Reid, and Corri. They all
mean much more to me than I can ever tell them using mere words.



Contents



        Acknowledgments

        Introduction

SECTION I  Getting Started
    1  The C Compiler System
    2  Program Development
    3  The DOS-to-C Connection

SECTION II  Standard Libraries and Interfaces
    4  Using Standard Libraries
    5  PC Operating System Interfaces
    6  The User Interface
    7  Automatic Program Configuration

SECTION III  File-Oriented Programs
    8  Basic File Utilities
    9  File Printing
    10  Displaying Non-ASCII Text

SECTION IV  Screen-Oriented Programs
    11  Screen Access Routines
    12  Buffered Screen-Interface Functions
    13  The ANSI Device Driver
    14  Viewing Files

SECTION V  Appendixes
    A  Microsoft C Compiler, Version 4.00
    B  Other C Compilers
    C  Characters and Attributes
    D  Local Library Summary

    Index



Acknowledgments

        Working with the management and staff at Microsoft
Press has been a distinct honor and a rewarding experience
in many ways. I am impressed by the dedication, skill, and
knowledge of all the individuals that I have dealt with
either directly or indirectly during the development and
preparation of Proficient C.
        The person who is responsible for suggesting this book
to me is Claudette Moore. She helped me pare down the
original outline to something manageable, provided all the
needed software, arranged for Microsoft experts to answer my
questions, and kept things moving in the right direction.
        Marie Doyle had the primary responsibility for editing
the manuscript. She has been tireless in her efforts to
obtain consistency and accuracy of the material and to
ferret out my hidden assumptions. Thanks also to Dori
Shattuck who participated in the early editing of the
project.
        I am indebted to Jeff Hinsch for poring over all the
figures and source listings, testing all programs to verify
correct operation, and working with me at some pretty
strange hours to transfer files electronically.
        Jim Beley has been involved throughout the project. He
was best known to me as the hit man ("Just calling to check
on our schedule ... "), but his efforts and those of his
staff are essential to the preparation and delivery of a
book. Jim made many helpful suggestions along the way to
improve the book and assure its timely delivery.
        I also wish to thank Reed Koch, who reviewed the
manuscript and contributed many useful comments and
suggestions on the manuscript and the programs.
        Many other individuals who have devoted themselves to
this project will probably never be known to me by name. I
appreciate their efforts nonetheless.



Introduction



        Building a house that someone is willing to live in is quite an
undertaking. The process starts with a good design that makes sense in the
context of its surroundings and proceeds to completion through a series of
carefully planned steps. Throughout the process, the builder must select
the right materials and must employ the right tools to shape and join those
materials.
        A computer programmer faces a similar challenge. He or she needs good
tools and needs to use them as effectively as a craftsperson in any other
profession or trade. Rather than hammers and saws, the programmer needs
assemblers, compilers, linkers, debuggers, and many other specialized
tools. In addition, the computer programmer can benefit from the work of
others by examining and using collections of ready-to-use program modules.


Objectives of This Book

        Proficient C is a book for programmers. It is primarily about writing
programs that are good tools for programmers. It demonstrates the use of C
in the development of nontrivial applications, so we will not waste any
time solving the Fibonacci series or calculating factorials. Although
programs that do such things are instructive, they are of little value to
us in building our tools.
        Proficient C presents proven techniques for developing programs that
solve real problems. Now, any person with a modicum of business sense knows
that a problem is simply an opportunity in disguise, and some programmers
have been astute enough to detect this connection and turn it into piles
of hard cash. Whether or not money is your motivation for programming is
unimportant; if you are going to program for any reason, you should try to
do it well. This book is designed to help you do just that.
        We will focus on creating building blocks--reusable modules that have
wide applicability. Being frugal with our time and energy, we will use
existing modules, such as those in the standard libraries that accompany
most C compilers, as much as possible. And, of course, we will use our own
modules again and again as we develop increasingly sophisticated programs.


Intended Audience

        Programmers with at least a modest level of experience in some high-
level language or assembly language will have an easy time going through
this book. A working knowledge of C or a similar language (such as Pascal,
ALGOL, Modula 2, or Ada) is assumed. Those readers without such experience
will be best served by first reading an introductory text on C. Several
good beginner- and intermediate-level C books are listed at the end of this
introduction.
        I don't share the opinion that BASIC programmers are somehow crippled
by the experience of programming in BASIC, but I admit that many will find
some of the concepts embodied in C to have an alien feel. Topics like
indirection and recursion don't become immediately clear to most
programmers who encounter them for the first time. Study and practice are
required in order to become comfortable with them.


Hardware

        All functions and programs in this book were developed and tested in
an MS-DOS environment on an AT&T PC6300, and in a PC-DOS environment on an
IBM PC/AT. The programs will probably also work on other compatible
hardware that runs MS-DOS version 2.00 or later. With suitable compilers,
many of the programs in this book that are not dependent upon specific PC
features can easily be ported to other hardware and operating systems.
That's one of the many beauties of C.
        You are well advised to have as much primary and secondary storage as
possible. A comfortable amount of random access memory (RAM) is 512
kilobytes. This allows room for a RAM disk to speed some operations. A hard
disk is also recommended. These recommendations are based on the fact that
most modern C compilers in the PC marketplace are large and are usually
accompanied by massive libraries of standard functions. C compilers will
function on less well-endowed systems, but the cost is usually diminished
performance.


Software

        Many of the programs in this book require MS-DOS or PC-DOS, version
2.00 or later. For the most part, I will refer to DOS in the generic sense.
I will use the terms MS-DOS and PC-DOS in contexts where the difference is
noticeable. (There are very few of these, limited mostly to a few external
commands that may be available in one version and not in another.)
        A good text editor is critical to success in large programming
projects. You can use the DOS EDLIN editor if you're a bit of a masochist,
but I recommend that you acquire a good programmer's editor like EDIX
(Emerging Technology Consultants, Inc.), Brief (Solution Systems), or
Epsilon (Lugaru Software, Ltd.). You may also use Microsoft Word, WordStar,
or any other word processor that permits files to be saved in a straight
ASCII (unformatted) form.
        Without a C compiler, of course, this book will be of little value to
you. The programs were written using Microsoft C, version 4.00, but other C
compilers can be used instead. I have tried to minimize the use of features
of one compiler that would prevent the use of others of comparable
capability. Appendix B describes several other C compilers and what, if
anything, must be done to compile the programs presented in this book
using those compilers.
        The DOS linker (LINK) provided with the operating system is used to
combine object files and library modules into executable programs. A
version of the linker that understands DOS pathname conventions is a
necessity. Microsoft provides updated versions of the linker with all of
its language products.


Organization

        Section I describes the C compiler system and presents some views on
program development. In Chapter 1, we look at the proposed ANSI standard
for C and at compilers that attempt to track this moving target. Chapter 2
explores ways in which program development can be made more of a science
than an art. Chapter 3 shows how C is used in a DOS setting with emphasis
placed on effective use of the DOS command-line and environment variables.
        Section II describes standard libraries and interfaces. Chapter 4
focuses on the use of the many existing functions in the standard C
libraries. In Chapter 5, we explore the low-level interface to DOS from
our C programs. Although not portable to other operating systems, such as
UNIX/XENIX, these functions are important for certain classes of programs
running on the PC. Additional functions built on top of the standard
libraries are used at a higher level to manage the all-important
user/machine interface. These functions are the topic of Chapter 6. In
Chapter 7, we automate program configuration, but allow the user to
override the automatic settings in various ways.
        Section III presents a useful set of file-oriented programs.
Chapter 8 provides several UNIX-like file and directory utilities,
including an LS program to list a directory in special formats, a TOUCH
command to assist the MAKE program maintainer, and an RM command. This
orientation to basic file utilities leads to the development of a versatile
print command (PR) in Chapter 9. Chapter 10 deals with ways of viewing
the contents of non-ASCII files.
        Section IV switches gears, leaving the realm of line-oriented programs
to address the issues of screen-oriented programs. Chapter 11 introduces
the concept of a buffered screen interface, and the concept is brought to
fruition and expanded upon in Chapter 12, which covers screen-management
functions. Chapter 13 describes an interface package that uses the much-
maligned ANSI device driver provided with DOS. Chapter 14 presents a
visual means of viewing files.


Other Books on C

        Many introductory-level C books crowd the shelves in bookstores, with
most of the books introduced in the past four or five years due to the
rapidly increasing popularity of PCs and to the "discovery" that C is an
extremely compliant and versatile computer programming language. Here is a
list of some C books that my students and I have found to be valuable
learning aids.
        The C Primer, 2nd Edition, by Les Hancock and Morris Krieger (McGraw-
Hill, 1985), is noted for its gentle introduction to programming in C. The
use of flowcharts and examples based on models familiar to everyone makes
this an easy-to-read and enjoyable book.
        Programming in C, by Stephen G. Kochan (Hayden, 1983), has been my
mainstay as a teaching book for beginning programmers. It covers the
essentials of C without getting bogged down in superfluous detail.
        C Primer Plus by Mitchell Waite, Stephen Prata, and Donald Martin
(SAMS, 1984), is replete with corny but memorable examples and
illustrations. It is a comprehensive book that gets a bit deeper into C
language than some of the other beginners' books.
        In addition to these introductory books, you may want to take a pass
through Variations in C by Steve Schustack, another C book from Microsoft
Press (1985). It is oriented toward developers of business applications
and is loaded with advice on good programming style and many very useful
examples.
        And every C programmer should have a copy of the ubiquitous The C
Programming Language by Brian Kernighan and Dennis M. Ritchie (Prentice-
Hall, 1978). Although it is somewhat dated now, it is a treasure trove of
C lore and law and is still considered the standard reference on the C
language.


Notational Conventions

        The following notational conventions apply to the text and examples in
this book:

        ►  DOS program names are shown in capital letters (DIR, TYPE). The
names of programs we develop are treated the same way (PR, VE, NOTES) when
the executable program is being referred to.

        ►  Names of C functions, variables, and keywords are shown in italics
(environ, fgets(), main(), #define). This same treatment is given to the
names of source and object files (myprog.c, myprog.obj) and user-supplied
function, constant, and variable names.
        ►  Command syntax follows these conventions:


═══════════════════════════════════════════════════════════════════════════
ITEM                CONVENTION                 EXAMPLE
───────────────────────────────────────────────────────────────────────────
literal text     │  roman type              │  cmd
placeholders     │  italic type             │  cmd option
repeat item      │  ellipses                │  cmd file . . .
optional item    │  square brackets ([])    │  cmd [opts] file
logical OR       │  vertical bar (|)        │  cmd [a | b] file . . .
───────────────────────────────────────────────────────────────────────────



SECTION I  Getting Started



Chapter 1  The C Compiler System



        The tradition of C, originally established in its UNIX context, has
had a profound effect on the programming environments of a vast range of
machine types and sizes and has influenced the gamut of computer operating
systems. Whether you program on an S-100-bus 8080 system running CP/M or on
a CRAY-2 hosting a version of UNIX System V, or nearly anything in between,
there is probably a C compiler system available for your environment.
        In this chapter, we will briefly describe the proposed American
National Standards Institute (ANSI) standard for C, some emerging standards
for operating systems, and extensions of those that affect our C
programming. Then we'll get a brief overview of the Microsoft C Compiler,
its various memory models, and C programming support tools.


Standards and Compatibility

        Anything that survives the test of time and that is likely to have a
wide base of support is a candidate for standardization. As standards
evolve, de facto or otherwise, we face the issue of compatibility with the
standard. So it is with C.


        Proposed ANSI-Standard C

        The first attempt at standardizing C was the distribution of the "C
Reference Manual" written by Dennis M. Ritchie, the author of C. The "C
Reference Manual" was published as an appendix to the book The C
Programming Language by Brian Kernighan and Dennis M. Ritchie. For many
years, this book has been the only generally available description of C.
However, since the "C Reference Manual" was promulgated, there have been
numerous changes to C that are not supported by all C compiler suppliers,
so portability among computer systems, and even among compilers on the same
computer system, has been diminished.
        The /usr/group Committee produced the "UNIX 1984 Standard" document to
attempt standardization of the UNIX operating system. One major aspect of
the /usr/group proposal is a standard set of library routines. Many of the
recommendations, but not all of them, have been incorporated into AT&T's
"System V Interface Definition," which defines a standard base for, among
other things, the Operating System Services and Other Library Routines, the
two major components of the standard library for C. The current de facto
standard for C is the UNIX System V implementation by AT&T.
        In recent years, the American National Standards Institute's Technical
Committee on the C Programming Language--Committee X3J11--has been working
to produce the first official American National Standard for C. Committee
X3J11 is composed of members from various industrial companies, educational
institutions, and government agencies.
        The proposed ANSI standard for C is an attempt to "promote
portability, reliability, maintainability, and efficient execution of C
language programs on a variety of computing systems." The draft standard
uses the "C Reference Manual" and the "UNIX 1984 Standard" as base
documents for the language and the library, respectively.
        The following list describes some of the features that have been added
to C or changed since its original description and points out some of the
implications of the new features and changes.

        ►  The enum type specifier has been added, giving C an enumeration
        data type.

        ►  The void keyword can be applied to functions that do not return a
        value. A function that does return a value can have its return
        value cast to void to indicate to the compiler (and lint, under
        UNIX/XENIX) that the value is being deliberately ignored.

        ►  Structure handling has been greatly improved. The member names in
        structure and union definitions need not be unique. Structures can
        be passed as arguments to functions, returned by functions, and
        assigned to structures of the same type.

        ►  Function declarations can include argument-type lists (function
        prototyping) to notify the compiler of the number and types of
        arguments.

        ►  Hexadecimal character constants can be expressed using an
        introductory \x followed by from one to three hexadecimal digits
        (0--F). Example: 16 decimal=\x10, which can be written as 0x10
        using the current notation.

        ►  The # in preprocessor directives can have leading white space (any
        combination of spaces and tabs), permitting indented preprocessor
        directives for clarity. In addition, new preprocessor directives
        have been added:

            #if defined (expression)
            #elif (expression)


        Standard Run-Time Library Routines

        The standard libraries that accompany most C compilers are standard
because we accept them as such, not because of any law. Over many years and
millions of lines of C source code, programmers have evolved a set of
often-used routines that have been polished and packaged into libraries.
Some of the routines, such as read() and write(), provide system-level
services and are essentially our hooks into the underlying operating
system. Other routines--string-handling functions and macros, for example--
are part of the external subroutine library. (In Chapter 4, we will examine
some representative standard library functions and present examples of
their uses in programs.)
        In the last few years, some new additions have been made to the
standard libraries. Among them are a set of buffer-management routines,
some new string-handling routines, improved I/O error reporting, and
additional debugging support routines. The proposed ANSI standard specifies
a basic set of system-level and external routines.


Microsoft C, Version 4.00

        The Microsoft C Compiler, version 4.00, adheres closely to the
proposed ANSI standard. I will briefly describe the compiler system and its
features, to set the stage for the programs that follow. Appendix A
provides a detailed description.
        The Microsoft C Compiler is a three-pass optimizing compiler. The
passes are named C1, C2, and C3. These passes could be called directly, but
in practice they should not be. We use one of the control programs MSC and
CL to invoke them for us. MSC is the primary compiler control program. With
MSC, the linker must be called separately to combine program object files
and library modules into an executable program. CL is similar in operation
to the UNIX/XENIX C compiler control program cc. It invokes both the
compiler passes and the linker, as needed, to produce an executable
program.
        The Microsoft C Compiler uses DOS environment variables to locate
libraries and header files in special disk directories. These are normally
set to \lib and \include, respectively. The compiler will use a designated
temporary area on disk if the variable TMP is defined. The temporary area
can be a virtual disk to speed disk-intensive processing.
        If the machine used at run time has a math coprocessor, the
coprocessor is used automatically. Floating-point emulation is used if no
coprocessor is installed. Thus, it is not necessary to have separately
compiled versions of the same program for differently configured
systems.
        The material in this book was developed on personal computers running
MS-DOS and the Microsoft C Compiler, version 4.00. But many of the programs
presented here can be moved with little or no change to other hardware,
other operating systems, and other C compilers. Appendix B describes
several other major C compilers and how they can be used to compile the
programs in this book.
        The primary areas that preclude unqualified portability to other
environments are related to the occasional use of special keys on the
PCkeyboard, to screen updating via BIOS and direct-access routines, and to
the use of other hardware elements of the PC, such as built-in timers and
reserved memory areas. Where possible, dependencies on PC hardware have
been avoided. One of the goals of the book is to show off some of the
marvelous capabilities of the PC and the Microsoft C Compiler, so by
design, some of the material is not portable to other environments without
some changes.


        Memory Models and Uses

        Microsoft C supports programs up to one megabyte in size. It provides
five primary memory models plus some extensions that let you produce mixed
memory models to meet special needs. The memory models are defined by the
following table:

═══════════════════════════════════════════════════════════════════════════
    MODEL                     SIZES
───────────────────────────────────────────────────────────────────────────
                Code        Data         Arrays
                ─────────────────────────────────────────────────────────────

small         │ 64 KB     │ 64 KB     │  64 KB
medium        │ 1 MB      │ 64 KB     │  64 KB
compact       │ 64 KB     │ 1 MB      │  64 KB
large         │ 1 MB      │ 1 MB      │  64 KB
huge          │ 1 MB      │ 1 MB      │  >64 KB
───────────────────────────────────────────────────────────────────────────


        Support Tools

        When we talk about a C compiler, we usually implicitly include a set
of support tools that make up a working C compiler system. We'll now
examine some of the important support tools that enhance the C programming
environment.

        LINK, the DOS Linker  Regardless of what language you program in, you
will use the DOS LINK program, the object linker provided with DOS.
Depending upon which version of DOS and which language product you have,
you may have to upgrade to a newer version of the linker. The linker must
understand DOS pathnames and, when used with the Microsoft C Compiler, must
be able to access the values in DOS environment variables that point to the
library and include file directories.
        The version of LINK that is used to link the programs in this book is
version 3.51. It was shipped with the version 4.00 C compiler. This version
of LINK provides a single-level overlay-linking capability that permits
memory space to be shared sequentially by several program modules.
Earlier versions of LINK, back to version 3.12, will also work, but they do
not have the overlay capability of the 3.51 version. I chose not to use
overlays in this book, so there is no requirement for an overlay linker.

        LIB, the Object-File Library Maintainer  A library in the computer
programming context is a collection of compiled or assembled functions. The
functions in a given library are typically related to each other by some
common factor, such as the role they serve in handling one major
programming task. LIB is provided with Microsoft language products. It
allows us to create, organize, and maintain run-time libraries.
        The linker calls on various libraries to resolve external names during
a linker session. The linker automatically looks for the standard C library
(slibc.lib, mlibc.lib, and so on) for the appropriate memory model. It
looks in the current directory unless the DOS environment variable LIB
tells it to look elsewhere, or you provide a list of other libraries to
search.
        We will make extensive use of the libraries provided with our
compilers, and we will create some libraries of our own. (Our library
strategy is documented in Chapter 2.) Nearly all of the service routines
we write in this book will end up in an object library somewhere. Appendix
D summarizes all object library functions developed in this book.

        MAKE, the UNIX-Style Program Maintainer  Any repetitive task begs to
be automated. MAKE is an automated program maintainer that emulates the
basic behavior of the UNIX MAKE program. I will present information on the
use of MAKE in building some of the programs in this book. Because my
programs, even some of the simple ones, are usually broken into a number of
small modules, it is important to have a tool that takes most of the
drudgery out of maintaining the programs as changes are made to them during
the development and lifetime maintenance periods.
        MAKE performs its magic by applying rules, both built-in and user-
supplied, to keep each object file up to date with its sources. The user-
supplied rules reside in a "makefile" that lists all the modules of a
program; the dependencies of objects on sources, header files, and
libraries; and the recipe for how to connect the pieces. MAKE is equally
able to control documentation projects and other activities that manage
groups of disk files.
        The next chapter describes the design and development process espoused
in this book and shows the relationships of the various activities involved
in building a program. MAKE plays an important role in the process.

        CodeView, the Symbolic Debugger  With the release of version 4.00 of
the Microsoft C Compiler, Microsoft unleashed CodeView, a spectacular
window-oriented, source-level symbolic debugger.
        Anyone who has done even a modest amount of programming knows the
sense of frustration that accompanies a bug hunt. A program that should
take a couple of hours to design, write, and test can cost you many hours
of anguish because of a subtle, hard-to-find bug. CodeView has the
debugging capabilities and operating features needed to shrink the
debugging task down to manageable proportions.
        The debugger is a logical extension of the DOS DEBUG program and
Microsoft's SYMDEB, so knowledgeable users of those programs will find
CodeView very easy to learn. Using multiple windows, CodeView allows you to
view source code, disassemble object code, and monitor program variables,
CPU registers, and the stack. Figure 1-1 is a sample CodeView screen that
reveals some of the primary features of the debugger.
        CodeView works on color or monochrome systems, can handle a 43-line
EGA (enhanced graphics adapter) screen, and supports both mouse and
keyboard user interfaces.

        MASM, the Macro Assembler  The Microsoft C Compiler produces object
files in the Microsoft object format, so there is no need to run a separate
assembler pass. For information about MASM and ASM, see Advanced MS-DOS by
Ray Duncan, Microsoft Press, 1986.

                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║    Figure 1-1 is found on page 10        ║
                ║    in the printed version of the book.   ║
                ║                                          ║
                ╚══════════════════════════════════════════╝

        FIGURE 1-1 ▐ Sample CodeView screen display



Chapter 2  Program Development



        Program development means different things to different people. The
particular interpretation often depends on whether the question is asked of
a programmer who always works independently or one who normally functions
as a member of a team.
        Whether done by an independent programmer or a team of programmers,
program development is a blending of art and science and is an iterative
process. For anything but trivial projects, we rarely know exactly how we
will do the job before actually starting to do it. We can usually make some
good guesses, but there is often an element of discovery involved. As long
as something is learned each time through the loop, and as long as mistakes
are not repeated, we will likely make some headway.
        The science part is somehow keeping ourselves headed in the right
direction, capturing needed information during the course of a project, and
effectively applying what is learned. The art is in knowing what to try
when all else fails and in being able to conjure up the correct data
structures and algorithms to solve seemingly intractable problems.
        There is usually at least one approach to solving a problem that makes
the task easier than do other approaches. Experience and imagination are
your two most important allies in pursuit of this approach. The experience
need not be all yours, either--try to borrow existing ideas and adapt them
to your purpose if there is a reasonable fit. And be pleased to have other
programmers borrow your ideas--it's a sure sign of success.


Guiding Principles

        During the past 10 years or so, I have had the good fortune to be both
an observer and a participant on solo and group programming efforts. As a
technical writer and publications supervisor, I was able to see the good,
the bad, and the ugly aspects of very large programming efforts. As a
developer, I saw firsthand how the development process can be made to work
well and how it can be made to fail pitifully.
        The consistent and careful application of structured design and the
incremental development of both programs and the documents that describe
them are critical factors in the success of programming projects. I suspect
that the exact method used has less bearing on the outcome of a project
than does the fact that the participants consciously attempt to control
what is going on rather than just letting it happen.
        This book encourages a heavy dependence on the development of reusable
modules. The concept of creating reusable modules--essentially stockpiling
programming pieces--seems obvious in retrospect, but it was quite a
revelation to a generation of programmers 10 to 15 years ago. The standard
library is a wonderful example of the concept coming to fruition. There is
something elegant about a function that, for example, copies a string onto
the end of another string and returns a pointer to the start of the
resulting string. Variations of this technique pervade the standard library
and give us options in using the library routines that we would not have if
the routines had been designed in a less general way.
        The point here is that we should keep an eye focused on the future use
of what we are doing now. First, strive to get something working, but don't
quit too soon. Refine it, generalize it, and make it available for future
use, both by yourself and by others. The extra time spent in this endeavor
will be paid back many times over in saved project time. Also, don't
neglect to document what you do, both in comments within the sources and in
separate manual-style documentation.


The Development Cycle

        The first step in the development cycle is determining what must be
done. This is the problem-definition phase and is the time when we
determine the user's needs, investigate the operating environment, and try
to get a feel for the boundary conditions that will influence our
design.
        The next phase, program design, is anything but a straightforward
process. We need to keep our minds open to many alternatives. We should
attempt paper designs, perhaps written in pseudocode, which is a human-
language description disguised as program code. Our hope is that at least
one of the paper designs will lead to a solution that can be implemented. A
major objective at this stage is to produce a language-independent solution
to the problem at hand so that we have options later, in terms of choosing
a language that best fits the circumstances.
        When we have what we believe is a workable solution, we can select a
language and start coding. I prefer to use C because of its compactness and
versatility, but I sometimes use Pascal, Assembler, or BASIC in my work.
The choice should be based on factors such as the "lingua franca" of the
host environment (local custom), whether any work has already been done on
the problem in a particular language (invested value), and whether any of
the current investment is worth saving (sometimes it isn't). But telling a
hundred assembly-language programmers that from today forward all work will
be in C is potentially risky business. It might be easier to learn yet
another assembly language!
        The next phase of the program-development cycle is an important one
that is often overlooked. Testing should be planned right along with
development, and should be done either by the programmer of the item to be
tested or by an associate working closely with the programmer. As soon as
the code is done, there should be a way to exercise it to see that the
program does what it is supposed to do, doesn't do anything it shouldn't,
and protects itself from possible adverse effects of unwanted inputs.
        It is not too hard to check a program for correct behavior in the
presence of expected inputs. This is, of course, important. But the
behavior of a program in the face of unexpected inputs is equally
important. Writing a program that defends itself well is a bit of a
challenge and requires a somewhat perverse mentality on the part of the
programmer and the tester.
        Checking boundary conditions, handling errors promptly and correctly,
and generally just being on guard for the conditions that could make your
program crumble is something that requires discipline and practice. Be sure
to check the return codes from routines that flag errors. Standard library
I/O routines are particularly good about telling you when something goes
wrong (full disk, bad filename, and so forth). Don't let this vital
information fall on the floor.
        As I said earlier, the program-development process is an iterative
one. At any point in the process, don't be afraid to pull the plug and
start over if you find yourself traveling the wrong path. If necessary, go
to completion, refine the original definition, and do the job again until
it's right. You'll be glad you did.


Our Local Environment

        For the purposes of our development work in this book, we will set up
some directories for header files and libraries. The hardware and software
assumptions listed in the Introduction apply. We will use the Microsoft
recommendations for the standard run-time libraries and supplied header-
file locations. Any header and library files that we create will be placed
in the new directories \include\local and \lib\local, respectively. This
practice precludes one of our homegrown files from clobbering one of the
files provided with the compiler if we accidentally use the same name. It
also collects all of our own files together for convenient access.
        We will be preparing several header files as we go on through this
book. For now, we'll start with the file std.h, which contains some basic
information needed by many of our C source files.
        In addition to defining some often-used constants, std.h contains a
definition of a Boolean data type (using the enum type specifier) and some
aliases for standard data types that have long names (unsigned short, for
example). I used to use these often, but as my typing speed increased, I
began to use the original names. Feel free to use whatever you find most
natural (or easiest to type).
        We can use the preprocessor to retrieve our local header files in a
way that is consistent with the approach used for standard header files.

        The preprocessor directive

        #include <local\std.h>

tells the preprocessor to read in the text of the file \include\local\std.h
because we previously set the DOS environment variable INCLUDE to
\include.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 15 in the printed version of the book.   ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        Similarly, our local library directory will eventually contain a
utility library, util.lib, a pair of operating-system interface libraries,
dos.lib and bios.lib, and several other special-purpose libraries. The
linker must always be given the correct name for the directory that
contains the library files. We will arrange for this to be done
automatically by using a makefile that includes pathname specifiers for the
required directories. Then we'll let MAKE do all the hard work. Isn't high
tech wonderful?
        Now it's time to do some programming. We begin in Chapter 3 with an
examination of how our C programs interact with the MS-DOS system.



Chapter 3  The DOS-to-C Connection



        DOS is, of course, the very popular and widely used operating system
for the IBM PC and other personal computers that have a comparable 16-bit
architecture. Proficient C is designed to help you write C programs in a
DOS environment.
        To obtain the greatest benefit from our PCs, we must understand both
the C language and the DOS environment in which we program. In addition, we
would like to exploit the similarities among DOS, UNIX, and XENIX for some
classes of programs, especially those that don't require the use of
specialized hardware or direct screen access.
        From the beginning, DOS has had some features in common with UNIX and
its many descendants, including XENIX. Among these similarities are basic
aspects of command syntax (copy from to, rather than the "backward"
approach of CP/M); the use of a common "file" orientation for all data
streams, including the console (keyboard and screen) and the printer; and a
batch-processing capability (.BAT files under DOS and shell scripts under
UNIX). Starting with version 2.00, the operating system acquired many more
features that are reminiscent of those provided by UNIX. Among the new
features were a hierarchical file system and related support for a hard
disk, installable device drivers for customization, a user-controllable
environment, and input/output (I/O) redirection. These important features
added greatly to the power and flexibility of DOS.
        In this chapter, we will examine I/O redirection, command-line
processing, and access to the DOS environment. Some aspects of each of
these topics will be involved in the construction of the programs that are
presented in the remainder of this book.
        Proficient C makes extensive use of the routines in the C run-time
library. In this chapter, we use only a few of the more common library
routines. A detailed discussion of the C run-time library is presented in
the next chapter (Chapter 4). We defer discussion of device drivers until
Chapter 13.


Input/Output Redirection and Piping

        By way of a few simple examples, we will quickly review some of the
basics of input and output and set the stage for considerably more detailed
work using the I/O features of the C run-time library, a marvelous
collection of prepackaged routines.
        Program I/O is based on the concept of standard streams: standard
input (stdin), standard output (stdout), and standard error (stderr). The
stdin stream is usually "connected" to the keyboard, and both the stdout
and stderr are usually connected to the screen. Under DOS, both stdin and
stdout can be redirected to other devices, but stderr always goes to the
screen.
        Because all references to I/O devices are treated like files under
DOS, we can make ad hoc changes in the source or destination of data being
processed by a program. The output of the DIR command, for example,
normally goes directly to the screen (stdout). But with a minor change to
the command line, we can send the directory listing directly to a printer
by redirecting the DIR command's standard output. The command

        dir > prn

tells DOS to feed the data that DIR normally sends to the console screen to
the default printer device instead. Thus, the standard output of DIR is
redirected.
        We can similarly redirect input to a command. The DOS MORE command is
a good example. MORE is classified in the DOS manual as a filter--a program
that is designed to read from the standard input device, perform a
transformation of some kind on that input, and produce some output on the
standard output device. The transformation performed by MORE is to parcel
out the input it receives in screen-sized chunks. MOREcan be used as the
termination of a pipeline--a series of programs connected to each other by
a pipe, symbolized by the vertical bar (|). The DOS pipe is actually
implemented as a pair of redirections. The output of one program is
redirected to a specially named disk file, which then becomes the input
to the next program in the pipeline.


        ┌────────────────────┐                             ┌─────────────┐
        │ ┌────────────────┐ │                             │             │
        │ │                │ │                  stdout     │             │
        │ │                ├─┤◄────────────────────────────┤             │
        │ │     screen     ├─┤                  stderr     │             │
        │ │                │.│◄────────────────────────────┤             │
        │ └────────────────┘▓│                             │             │
        └────────────────────┘                             │             │
                                                        │   program   │
                                                        │             │
                                                        │             │
        ┌───────────────────────────────┐                 │             │
        │┌─┬────────┬────────┬─┐        │                 │             │
        ││▒│▒▒▒▒▒▒▒▒│▒▒▒▒▒▒▒▒│▒│  ┌────┐│                 │             │
        ││                     │  │    ││      stdin      │             │
        ││▒  keyboard  ▒▒▒     └┐ │▒   │├────────────────►│             │
        │└┐▒▒▒             ▒┌┐▒┌┘ │ ▒▒ ││                 │             │
        │ └─────────────────┘└─┘  └────┘│                 │             │
        └───────────────────────────────┘                 └─────────────┘

        FIGURE 3-1 ▐ Default standard I/O streams


        The ability to join individual programs into custom-designed
collections via the pipe mechanism is an important consideration in the
design of an operating system. We can easily plug the output of one program
into the input of another to perform special processing. To control the
output of the TYPE command, for example, we issue the following
command:

        type myfile.txt | more

This command will display no more than a full screen of text at a time,
allowing the user to page through the text at a leisurely pace rather than
see the text flash by and scroll off the screen before it can be read.
        The same effect can be achieved by redirecting input to MORE using the
command

        more < myfile.txt

We have redirected the standard input of MORE in this case.

        To illustrate the use of redirection and piping techniques, here
is a program called CP, which is a highly simplified version of the DOS
COPY command. It is written as a filter in that it copies its standard
input (stdin) to its standard output (stdout) one character at a time.
Strictly speaking, however, CP is not a filter, because it simply passes
its input to its output without modification.

/*
    *      cp -- a simplified copy command
    */

#include <stdio.h>

main()
{
        int ch;

        while ((ch = getc(stdin)) != EOF)
                putc(ch, stdout);
        exit(0);
}

        This program should look familiar to anyone who has studied C
programming at any level. Although very simple, CP allows us to copy a file
from one place in a file system to another with ease. It has some serious
limitations that we will address as we proceed through this chapter--for
one thing, CP does no error checking, so it depends on DOS to do that work
on its behalf. In addition, CP takes no filenames on the command line, so
all input must be via redirection, which requires extra typing.
        CP simply gathers a single character at a time from the standard input
and checks to see whether it is the EOF (end-of-file) indicator. If the
input is not EOF, it is immediately sent to the standard output. If EOF is
detected, the processing loop terminates, and so does the program.
        EOF is defined in stdio.h as -1. This is why the character-storage
location ch is declared to be an int instead of a char data type. Since
char can be signed or unsigned (depending on system implementation), we
must always be prepared to detect the EOF flag to terminate the copying
process.
        The getc() and putc() library routines are implemented as macros and
are defined in stdio.h. They are fast and use the standard I/O library
user-buffering scheme. A buffer of BUFSIZ bytes (512 bytes in the Microsoft
C implementation) is used for reading and writing operations. When getc()
attempts to read past the end of an input stream, it returns EOF.
        There are several ways to use CP. Simply typing CP on the DOS command
line causes the program to take input from the keyboard and display a copy
of it on the console screen. You will see the text you type twice, once as
you type it, because DOS is echoing your input, and a second time after you
type a carriage return to end your input. User input may be terminated by
typing Ctrl-Z followed by a carriage return.

C>cp
This is a line of text.
This is a line of text.
^Z

C>

        To copy the contents of a file to the screen, you type the
command

        CP < filename

just as you would with the MORE command. To create a new file (or update an
existing file), you can type

        CP >> newfile

and CP will copy your keyboard input into newfile until you type Ctrl-Z
(the DOS end-of-file marker).
        Although this simple program is useful, it would be much more
versatile if it could accept optional parameters on the invocation
commandline, such as the names of files to process, and control information
to tailor the processing to meet user needs. The next section explores the
use of command arguments and applies them in several demonstration
programs.


Command-line Processing

        A program's invocation command line may contain optional arguments in
addition to the program name. The main() function of a C program is usually
written in such a way as to look first for optional arguments and then for
filenames or other parameters.
        Under UNIX, the convention is to start optional arguments with a
leading dash. DOS, however, uses a forward slash (/) to specify options, or
what are commonly referred to as switches. (UNIX uses the forward slash as
the pathname separator. Since DOS was not originally so close in spirit to
UNIX as it is now, DOS's use of the forward slash as a switch flag is
forgivable. However, this choice made it necessary to use something else
for a separator in pathnames when they were introduced in DOS version 2.00,
so DOS designers decided to use the backslash (\) as the pathname
separator.) I have elected to use the leading dash as an option flag, but
you can easily modify the code to allow the use of /, or to accept either
form, as is done in the Microsoft C Compiler programs.
        Arguments are passed to our programs by the C run-time startup
function. A count of arguments, argc, and an array of pointers to character
strings, argv, are passed as arguments to main() (along with envp, which
we'll look at in detail shortly). If a program needs to access the
arguments, it must include argc and argv in the definition of its main()
function:

main(argc, argv)
int argc;
char *argv[];
{
        /* function body */
}

        Figure 3-2 depicts how the command line and its components
are managed in memory. The command line can be thought of as having two
components: the command name itself (available within C programs only under
DOS version 3.00 and later) and the command "tail," which may contain
optional arguments such as filenames or input data. Only the command-name
component is required by DOS. The command-line text, if present, is
obtained from the DOS command-line tail. Information in the tail may be
optional or required, based on the design of the program being
executed.


        main (argc, argv)
    argc               program name
    ┌─────┐  ┌─────┬─────┬─────┬─────┬─────┐
    │  3  │  │  P  │  R  │  O  │  G  │ \0  │ (DOS 3.00
    │     │  │     │     │     │     │     │  and later)
    └─────┘  ─────┴─────┴─────┴─────┴─────┘
            │
            │                         command-line arguments
    argv    │  ┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐
    ┌─────┐  │  │  A  │  R  │  G  │  1  │ \0  │  A  │  R  │  G  │  2  │ \0  │
0│  ■──┼──┘  │     │     │     │     │     │     │     │     │     │     │
    │     │     ─────┴─────┴─────┴─────┴──────────┴─────┴─────┴─────┴─────┘
    ├─────┤     │                             │
1│  ■──┼─────┘                             │
    │     │                                   │
    ├─────┤                                   │
2│  ■──┼───────────────────────────────────┘
    │     │
    ├─────┤
3│  ■  │ NULL  (per proposed ANSI C standard, but not done by
    │     │        all C compilers--don't depend on it yet)
    └─────┘

        FIGURE 3-2 ▐ Command-line arguments


        Note the declaration of argv in the template for the main() function
shown above. The declaration

        char *argv[];

says that argv is an "array of type pointer to type char." This declaration
may also be written as

        char **argv;

which says that argv is a "pointer to a pointer to type char." The net
result is the same, but these are not identical declarations.


        Program Name

        The name used to invoke a program under DOS version 3.00 and later is
available to the program as argv[0] (or *argv, if the pointer has not been
moved). Earlier versions of DOS do not save the name used to invoke a
program, so most C compilers for DOS substitute a dummy value, such as a
null string or c. Being able to retrieve the invoking name can be very
useful at times, so if our programs detect the use of DOS version 3.00 or
later, we will let them use the name to their advantage.
        How can the name be used? For one thing, when many programmers are
producing programs in diverse locations for the same family of machines and
operating systems, it is likely that the same name will be used for two or
more programs that come into general distribution. We might like to be able
to rename one of two identically named commands to avoid problems. This is
easy to do if we have the source code, but it might not work if all we have
is the executable program. Moreover, some programs (usually copy-protected
programs) will not run if called by a different name. Another problem
occurs when a program is renamed because, in most cases, the program name
is hard-coded into error and help messages. So if we renamed CP.EXE to
CPY.EXE, we might still get an error message like cp: can't open file.txt,
which isn't nearly as useful as one that displays the currently used
program name.
        In Chapter 7, we'll find other uses for the command name. And
throughout the programs in this book, where it makes sense to do so, we
will try to obtain the invoking program name for use in error and help
messages.


        Ambiguous Filenames

        Microsoft C provides a set of functions to handle the processing of
wildcard characters in the formation of ambiguous filenames. Four object
files are provided, one for each major memory model, and they may be placed
in the default library directory for convenient access. The ssetargv.obj
file is used with small-model programs; the csetargv.obj, msetargv.obj, and
lsetargv.obj files are used with compact-, medium-, and large-model
programs, respectively.
        The setargv() function expands wildcard characters in the command-line
tail according to DOS rules (see the DOS manual for details) and passes
them to the program in the form of an argument list. It is then up to the
program to sift through the argument list it receives and separate optional
arguments from filename arguments.


        Accessing the Command Line

        The following program shows how to access command-line arguments,
including the program name. The REPLAY program first assumes that it will
be called by the name replay but, just in case, it checks to see which
version of DOS it is running under. If the DOS major number, obtained from
the _osmajor global variable defined in stdlib.h, has a value of at least
3, REPLAY calls getpname() to extract the filename part of the full
pathname passed by DOS. If the user renames REPLAY.EXE to something else--
say ARGLIST.EXE--then the program will issue the correct name when it
displays the banner line just before displaying the arguments, if any,
found on the command line.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 25 in the printed version of the book.   ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        Notice a few things in this demonstration program. We test for the
presence of arguments (if (argc == 1)) and quit with a nonzero exit code if
there are none. An argument count of 1 means that only the command name was
typed on the DOS command line. This simple precaution prevents us from
printing a banner line and nothing more, which would look kind of
silly.
        The temporary pointer p is declared as a pointer to a pointer to a
character and is assigned the initial value of argv. When we enter the
loop, argv is incremented so that it points past the command-name string to
the first optional argument. Each time through the loop, argv gets
incremented, so argv - p in the body of the loop yields the number of the
current argument and *argv yields the argument string. We could just as
easily have incremented p while leaving argv stationary, which would
require the argument-number calculation to be reversed (p - argv).
        The pgm character array is large enough to hold an eight-character
name plus a terminating null character. It must be declared static so that
it can be initialized to the expected name. We put a name in the array so
that if none is available from the operating system, we'll still have one
to work with--although there is a small risk that it may be the wrong one,
if someone has changed the program name.
        If _osmajor is 3 or more, we call getpname() to grab the base program
name out of the pathname string, which may be anything from a simple
filename to a full or relative pathname, complete with drive specifier and
extension. (DOS allows pathnames for programs in version 2.00 and later.
For the purposes of this book, earlier versions of DOS are considered to be
obsolete.)
        Here is the code for getpname():


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 26 in the printed version of the book.   ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        The program-name pointer, pname, accesses an array in the calling
function that is large enough to hold the filename part (eight characters)
of a valid DOS filespec, plus the terminating null byte.
        The getpname() function receives the full pathname of a file (path),
immediately sets cp to the start of path, finds the terminating null byte,
and backs up one position. It then moves backward to the start of the
filename, which is marked by the beginning of path if the name is a simple
filename, or by one of the accepted separators (:, \, or /). Notice that
the backslash has to be escaped (\\) to turn off its special meaning. The
last thing getpname() does is copy the filename part to pname, stopping
when it sees a dot or a null byte. (It is customary in programming to refer
to a single . as a dot.)
        The getpname() function is one we will use often, so it should be put
in a place where it can be retrieved easily. We will now start accumulating
useful object modules in a library of our own called util.lib, which will
reside in a directory called \lib\local on the default disk drive. (Users
of other compilers may need to make some adjustments if their compilers do
not allow this.) The function is first compiled using the command

        msc getpname;

and then the object file, getpname.obj, is added to the utility library by
typing

        lib \lib\local\util +getpname;

Any time we need to use this function, we add the library path to the list
of libraries to be searched by LINK.
        We can automate the process by using MAKE with a script in a makefile
routine such as the following one I assembled for the REPLAY program (the
use of the MAKE program maintainer is explained in Chapter 1):

# makefile for replay program

LIB=c:\lib

LLIB=c:\lib\local

replay.obj:     replay.c
        msc $*;

replay.exe:     replay.obj $(LLIB)\util.lib
        link $* $(LIB)\ssetargv $(LLIB)\util;

        This makefile, called replay.mk, is invoked using the command

        make replay.mk

The MAKE program will take care of the details of recompiling objects and
relinking the needed program modules to produce a new executable program.
This example is the paradigm for all the programs that follow. Simple
programs will be presented with specific compile and link descriptions; I
will present makefiles for the more complex programs in later chapters.


        Pointers and Indirection

        The source code presented thus far uses quite a few pointers. Many
programmers who are new to C language (and, for that matter, quite a few
who are not so new to it) have problems with the declaration and
application of pointers. Indeed, I have seen entire books written about C
programming that sidestep the issue of pointers by using indexed arrays
almost exclusively. That may seem a good way to minimize the difficulty one
experiences in learning C, but it robs the programmer of one of C's most
powerful capabilities and is really an unnecessary limitation.
        If you are having problems with pointers, try to visualize a pointer
declaration in the following way. (We'll use a pointer to a character here
because it is commonly encountered.) The line

        char *cp;

declares a character pointer. Place your finger tip over the cp part of the
declaration and see what's left (char *). Read this as "cp is a pointer
(the *) to a character storage location (the char)." In an expression,
then, the unadorned cp yields a storage location that holds a pointer to
(the address of) a character. If, however, you cover the *cp grouping, you
are left with the type char. This says that when you see *cp in an
expression, it yields a character storage location. You can retrieve the
value stored there and may assign a character value into the storage
location.
        Look back through the source for getpname(), which is loaded with
examples of the variable cp used in both contexts. Once you feel
comfortable with single indirection, as this is called, try the same thing
with double indirection, such as you encounter with the declaration of the
argument vector:

        char **argv;

The illustration in Figure 3-3 on the next page may help to clarify this
a bit. If it doesn't come easily to you, don't give up on it. A little
practice will make it clear. You have to get a picture in mind of what's
happening.
        In Chapter 6, we will examine user/machine interfaces further. We will
introduce a variety of ways of handling command-line arguments and talk
about getopt(), an AT&T library routine that attempts to standardize
command-line syntax.


                SINGLE INDIRECTION:
                    ┌──────────────┐ ┌────┬────┬────┬────┬────┬────┬────┬────┐
                    │      ■───────┼─►▒▒▒▒│▒▒▒▒│▒▒▒▒│▒▒▒▒│▒▒▒▒│▒▒▒▒│▒▒▒▒│▒▒▒▒│
                    └──────────────┘ └────┴────┴────┴────┴────┴────┴────┴────┘
                    char *s;
                            ░► s yields a pointer to a char
                        ▓▓► *s yields a char


                DOUBLE INDIRECTION:
┌──────────────┐ ┌──────────────┐ ┌────┬────┬────┬────┬────┬────┬────┬────┐
│      ■───────┼─►      ■───────┼─►▒▒▒▒│▒▒▒▒│▒▒▒▒│▒▒▒▒│▒▒▒▒│▒▒▒▒│▒▒▒▒│▒▒▒▒│
└──────────────┘ └──────────────┘ └────┴────┴────┴────┴────┴────┴────┴────┘
                    char **s;
                            ░► s yields a pointer to a pointer to a char
                        ▓▓► *s yields a pointer to a char
                        ▒▒▒► ** yields a char

        FIGURE 3-3 ▐ Pointers and indirection


DOS Environment Variables

        Another way to get data into a program or to modify a program's
behavior is by using variables that are maintained in the DOS environment.
To see which variables are in the environment and what their values are,
type

        set

at the DOS prompt and view the listing. All environments contain the values
of COMSPEC and PATH. On my system, the environment list looks like
this:


COMSPEC=C:\COMMAND.COM
PATH=C:\DOS;C:\BIN;C:\PWP;C:\WP;
PROMPT=$p$g
PWPLIB=c:\pwp
INCLUDE=c:\include
LIB=c:\lib
TMP=c:\
CONFIG=c:\config
FGND=cyan
BKGND=blue
BORDER=blue


        From the DOS command line or from within a batch file, a user can add
variables to the operating environment by using the SET command. The
command

        set var=val

instructs DOS to add the variable var to the environment and assign it the
value val. Within a batch file, the value of the DOS environment variable
var can be obtained by using the construct %var%. The following batch file,
SHOW.BAT, does this to display the values set for foreground and background
colors (see Chapter 13 to find out how these values can be used):

echo off
echo FGND = %FGND%
echo BKGND = %BKGND%

        The initial echo off is used to silence DOS, which displays each line
in a batch file before processing it, unless it's told not to. (The default
really ought to be echoing off, and many enterprising programmers have
patched COMMAND.COM for each version of DOS to disable this echoing.)

                                COMMENT
            The amount of environment space reserved by DOS (prior
            to version 3.2) is only 160 bytes. You may want to
            expand the default space by using SETENV, a utility
            program supplied with version 4.00 of the Microsoft C
            Compiler. Some other commercial software products
            include a program called ENVSIZE, or something similar,
            but not all C compilers have such a utility.

        We want to be able to selectively read DOS environment variables into
our C programs, and we may even want to change them during the execution of
our programs. The library functions getenv() and putenv() allow us to do
both of these tasks. The functions use an environment pointer to gain
access to the DOS environment.


        The Environment Pointer

        Access to the DOS environment is obtained through the global variable
environ, or the main() function argument envp. The header file stdlib.h
declares the environment pointer as

        extern char **environ

Thus, environ is an array of pointers to environment strings.
        To add a third argument to main() to obtain a pointer to the
environment, you would use the following form:

main(argc, argv, envp)
int argc;
char *argv[];
char *envp[];
{
        . . .
}

If envp is declared as an argument to main(), both argc and argv also must
be declared because of the way C function arguments are processed. Note
that the declarations for argv and envp could be written

        char **argv;

        char **envp;
because these are functionally identical to the previous
declarations.
        The envp argument to main() is simply a copy of environ at the time
the program began execution. If subsequent changes are made to the
environment, the global environ variable is kept current, but the value of
envp, which is local to main(), is not.

        Reading from the DOS Environment  To read the value of a DOS variable
into a C program, use the getenv() function, which returns a pointer to a
character string if the variable is defined, or returns NULL if it is
not.
        The following test program, SHOWENV, accepts a list of strings from
the user and checks each in turn to see if it is the name of a currently
defined environment variable. If it is, SHOWENV displays the string and its
value. If not, SHOWENV displays the message "var not defined" and moves on
to the next string, if any. If no arguments are given to the command,
SHOWENV prints out the entire environment list.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 33 in the printed version of the book.   ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        The library function strupr() is applied to the string being
processed, because DOS always converts the variable name to uppercase
letters before saving it in the environment. If strupr() were not used, no
match would be found if the user typed the string arguments in lowercase
letters.

        Writing to the DOS Environment  The process of modifying the DOS
environment list is analogous to reading it, except that the putenv()
function is called. A return value of 0 indicates success and -1 indicates
failure, usually due to a lack of memory in which to write the modified
environment. The program SETMYDIR attempts to add the variable MYDIR to the
DOS environment by calling setenv().


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 34 in the printed version of the book.   ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        Please note that putenv() modifies the environment in which the
current process is running (the environment inherited is a copy from DOS),
but it does not change the original DOS environment. To observe this,
compile the SETMYDIR program. Then run a SET command to list the starting
environment (assuming you don't already have a MYDIR variable), run the
SETMYDIR program, then run the SET command again. You'll see the MYDIR
variable come into existence while the program is running, then it will
disappear when the program stops executing.

                                CAUTION
            Be careful to assign values into the environment
            correctly. The form of the SET command is rigid. No
            white space (space, tab) is permitted on either side of
            the equal sign. If spaces or tabs are inserted, neither
            getenv() nor the %var% batch-file mechanism will
            recognize that the variable is defined.

        The ability to modify the environment passed to a running program is
important. A program can, for example, set up its own private PATH so that
it can access its own set of commands while running, without disturbing the
user's normal environment, which is restored when the program terminates.
        That's about it for the DOS-to-C connection via command lines and
environment variables. We'll be using these connections extensively in our
programs, so spend some time getting familiar with the techniques and
routines described in this chapter.
        In the next chapter we will take a much closer look at some of the
standard library routines that are part of the run-time library of the C
compiler package.



SECTION II  Standard Libraries and Interfaces



Chapter 4  Using Standard Libraries



        C has no built-in input/output capabilities; all I/O operations are
handled by external functions. Neither does C have any built-in
stringhandling facilities or mathematical functions. This is not a
deficiency, but rather it is a benefit that permits each C compiler
implementation to use features and capabilities of the host hardware and
operating system, while presenting the same external interface to programs.
All such facilities are external to C and are handled by routines
(functions and macros) that typically reside in special libraries, which
are collections of often-used routines.
        The UNIX heritage of C has produced a large body of ready-to-use
routines that have successfully made the trip to DOS despite some
significant differences in the underlying operating systems and their
supporting environments. The routines mask the differences by providing a
clean, consistent, and fairly well-documented interface to programs.
        In this chapter, we will explore a small selection of the hundreds of
routines in the standard C run-time library and use them to construct some
simple but useful programs. Other library routines will be introduced in
later chapters as the need for them arises.


Why Use Libraries?

        Simply put, the standard libraries give us a reasonable degree of
program portability, provide consistent and predictable interfaces, and,
most important, reduce programming effort. Out of the millions of lines of
C code that have been written over the years, certain operations have been
programmed over and over again. Just a few obvious examples include:
opening and closing files; reading and writing characters; detecting errors
and displaying relevant messages; splitting, concatenating, and copying
strings; and managing a hierarchy of directories and files.
        In the standard libraries, many of these operations have been highly
refined for speed, flexibility, and applicability to a wide range of
situations. I prefer to use something that exists, if it works, rather than
start from ground zero. Therefore, this book will concentrate on using the
run-time library provided with the compiler wherever the library routines
will do the job. We will develop mid-level and high-level routines that
depend on standard libraries to carry out the low-level work. In all but a
few special cases, we will avoid creating new low-level functions--it is
simply not necessary most of the time.
        The portability issue is often the basis of religious debate,
something I wish to avoid because it is a waste of valuable time and
energy. The portability I strive for in the C programs in this book is that
needed to move a program among the various MS-DOS and PC-DOS systems, to
extensions of the DOS environment such as Windows, and to a lesser degree,
to XENIX and other UNIX-like systems. In some cases, I will sacrifice
portability to achieve a high level of performance under raw DOS because
that's what will "sell" best for a given application.
        To go for complete DOS/UNIX transparency requires that some of the
nicer features of a PC must be forsaken so that programs will run on any
terminal type that can be connected to a multi-user system. In particular,
this usually means saying good-bye to the use of function keys, arrow keys,
attractive line-drawing characters, and fast screen updates. Going for the
best performance on a PC usually means sacrificing portability to UNIX for
such screen-intensive applications as a programmer's editor.
        However, careful design can minimize the effects of most of the
differences between DOS and UNIX. The use of standard libraries goes a long
way toward ensuring DOS/UNIX portability because of the effort Microsoft
and other C compiler vendors have placed on providing compatible libraries.
The differences among vendors' libraries usually occur in PCspecific low-
level interfaces. Although I will discourage the use of such PC-specific
functions in programs designed for general use, they are the subject of
Chapter 5 and will be used in some of our programs.


Exception Handling

        Some days, things just don't seem to go right. Ever had one of those?
Well, programs have days like that, too. As programmers, we have a
responsibility to our programs to build in some way of dealing with
unexpected events. This is called exception handling and is an essential
component in the development of commercial-quality programs.
        Exception handling has two parts: detection and recovery. The standard
library functions attempt to detect errors and inform calling functions
about them. It is up to us to build in the appropriate recovery procedures,
even if that simply means giving up on a task, which is sometimes all we
can do.
        One of the more common error conditions is the absence of a needed
file. The fopen() function, for example, tries to open a named file. If
asked to open a nonexistent file for reading, fopen() returns NULL instead
of a valid file pointer. What's the appropriate response to this error
condition? At the very least, we may want to inform the user about the
missing file. If the error occurs in a loop that is processing a list of
files, we may choose to print a brief but informative message ("file XYZ.C
not found") to stderr before moving on to the next name in the list. In
other circumstances, it may be best to print the message and terminate
processing with an ERRORLEVEL (exit code), to tell DOS that something is
wrong.


        Standard Library Error Functions

        A set of standard library functions assists us with exception
handling. A global variable, errno, holds the number of the most recent
error. This number is used as an index into an array of error messages,
sys_errlist. (The message associated with error number 0 is "Error 0," a
catchall for errors not described elsewhere.) Rather than access errno and
sys_errlist directly, we can call on perror() to print the error message to
the standard error output. The manual page summary of perror() is:

        #include <stdlib.h>
        void perror(string);
        char *string;

where void indicates that there is no return value and string is a pointer
to a message that we provide.

                                COMMENT
            Compilers that do not adhere completely to the proposed
            ANSI C standard may not support the void data type (or
            lack of type, in this case) for functions. It may be
            necessary to use the following typedef to compensate
            for this deficiency:

            typedef int void;

            Calls to
            void functions will actually return an integer that can
            safely be ignored.

        The output of perror() is the string we provided, followed by a colon,
a space, the text of the system message for the most recent error, and a
trailing newline, all sent to stderr. Thus, the statement

        perror("File error");

if triggered by an attempt to open a nonexistent file for reading, produces
the following output on the screen:


File error: No such file or directory


        Appendix A of the Microsoft C Compiler Library Reference Manual lists
the errno symbolic constants used under MS-DOS, the message text for each,
and a description of each error. The DOS error messages are necessarily a
subset of the error messages used by UNIX and XENIX. See your own
C compiler library documentation for a detailed description of each error
and message.

        Ambiguous Return Values  Two macros, ferror() and feof(), are used to
resolve an ambiguity that is produced by certain standard library routines:
Because they are designed to return a pointer to a character, functions
like fgets() (which we will use in an example shortly) must return NULL to
indicate either that an error has occurred or that the end of the file has
been reached. We need to take additional steps to determine which condition
caused the NULL return value before we take any action.
        The manual page summary of ferror() is

        #include <stdio.h>
        int ferror(stream);
        FILE *stream;


where stream must be the file or stream associated with the NULLreturning
routine. The summary for feof() is the same except for the macro name. The
ferror() macro returns a nonzero value to indicate that an error condition
exists and feof() returns a nonzero value if the end of the file has been
reached, thus resolving the ambiguity of the NULL value.
        The following code fragment illustrates a way of handling the
ambiguous return:

        .
        .
        .
errcount = 0;
while ((s = fgets(line, BUFSIZ, fp)) != NULL) {
        /* process the line */
                .
                .
                .
}
if (ferror(fp)) {
        perror("Read error");
        clearerr(fp);
        if (++errcount >= MAX_ERR) {
                fprintf(stderr, "Too many errors\n");
                exit(1);
        }
}
        .
        .
        .

        Notice the use of clearerr() in the preceding fragment. Once an error
occurs in a stream, the error indication remains until the stream is closed
or until the indication is intentionally cleared by using either clearerr()
or rewind(). In the foregoing example, we clear the error indication and
allow some maximum number of errors (MAX_ERR) before giving up. On the
other hand, if the NULL is the result of having reached the end of the
file, we fall through to other processing.
        A common programming procedure is to display an error message and then
call exit() to flush output buffers and return to the operating system with
an exit code that indicates an abnormal termination. For convenience, we
will package these statements into a function called fatal(), which
displays the program name, the message text, the system error message, and
a newline. This will usually provide enough information to the user to help
determine the cause of the abnormal exit.

/*      fatal -- issue a diagnostic message and terminate  */


#include <stdio.h>
#include <stdlib.h>


void fatal(pname, mesg, errlvl)
char *pname;    /* program name */
char *mesg;     /* message text */
int errlvl;     /* errorlevel (exit code) */
{
        fputs(pname, stderr);   /* display error message */
        fputc(':', stderr);
        fputc(' ', stderr);
        fputs(mesg, stderr);
        fputs('\n', stderr);


        /* return to DOS with the specified errorlevel */
        exit(errlvl);

}


        The fatal() function doesn't return to the calling function, hence the
void return type. The errlvl value should be nonzero--a small positive
integer--to indicate an abnormal termination. The specific values used for
errlvl should be documented in a manual page for the program.

        Operating System Interrupts  Another type of exception is an interrupt
from the operating system, usually caused by the user pressing Ctrl-Break
or Ctrl-C. The library function signal() can be used to control how such
interrupts are handled. The summary for signal() is

        #include <signal.h>
        int (*signal(sig, func))();
        int sig;
        int (*func)();

This bizarre-looking declaration is not a misprint. The line

        int (*func)();

declares a pointer to a function that returns an integer. This is not the
same as

        int *func();

which declares a function that returns a pointer to an integer. The
declaration of signal() also is a pointer to a function that returns an
integer.
        The only value for sig permitted under DOS is SIGINT, #defined in the
signal.h header file. SIGINT is the DOS Ctrl-Break interrupt. Other
signals, including SIGHUP (hangup) and others that apply under the
multitasking UNIX and XENIX operating system, currently have no meaning
under DOS.
        One of three responses to the interrupt can be specified in the func
argument. A value of SIG_IGN causes the interrupt to be ignored. We might
choose to ignore the DOS Ctrl-Break interrupt during all or a portion of
the lifetime of a program to prevent the user from aborting some critical
operation. For example, it is unwise to let the user break out of a text
editor without first presenting the opportunity of saving changes that were
made to the editing buffer--it could be that the user was fumbling for,
say, Ctrl-D to move the cursor right and hit Ctrl-C by mistake. I know,
I've done it myself!
        Second, a func value of SIG_DFL causes the running program to abort
upon receipt of the interrupt, closing all open files and returning control
to DOS. Buffers are not flushed. This is usually what will happen if you
don't use signal(), unless you are using DOS functions that ignore Ctrl-
Break by design (see Chapter 5).
        A third type of response is to provide a pointer to a signal-handling
function. This is referred to as signal catching and is more of an art than
a science. The safest method of signal catching simply cleans up any work
in progress and makes a graceful exit. It either accepts the default
treatment or, in a few cases, simply ignores the interrupt.
        A return value of -1 from signal() flags an error, errno = EINVAL,
which says that the sig argument is invalid. This identifies a programming
problem that should be corrected before any user ever sees the
program.


Time Functions

        The standard library provides several routines for retrieving and
converting time values. The UNIX ctime subroutine package and the time()
system call are available as functions in the Microsoft C run-time library
for DOS. Because these functions have received very little attention in
books about DOS, we will give them a moment in the spotlight.
        The time() function returns the number of seconds that have elapsed
since 00:00:00 GMT (Greenwich mean time) on January 1, 1970. This date and
time is called the epoch. GMT is now officially called Universal time (UT),
but the use of the term GMT is likely to continue indefinitely, just as
core is still used to mean main memory, even if it's all solid state. File-
modification times and other times and dates that are visible to the user
are presented as local time values.
        The declaration of time() is

        #include <time.h>
        long time(timeptr);
        long *timeptr;    /* (usually NULL) */


The time() function takes a pointer to long as an argument and produces a
long return value. The argument can always be the NULL pointer. Before C
had a long data type, it was necessary to provide a suitable buffer for the
time value. Passing the address of the buffer synthesized a "call by
reference" (C uses "call by value" for parameter passing). Now the
application of time() simply assigns the return value to a long
integer.


        Time Conversions

        The long time value is wonderful for the computer, but we humans don't
often tell someone what time it is as a number of seconds since the epoch.
A more convenient notation for us is a character string that spells out, at
least as abbreviations, what the date and time are. We need some convenient
way to make a conversion from the long value to a character string. We also
have to deal with Universal time and local time and with the complications
of daylight saving time.
        Figure 4-1 shows the relationships of the time() function
and the functions that constitute the ctime subroutine package. Two
functions, gmtime() and localtime(), can be used to convert values
from the long result of time() to time structure pointers. The DOS time
structure template, struct tm, defines a set of variables of type int that
can hold hour, minute, second, month, day, and year values, plus a flag for
daylight saving time and numbers for day-of-week and day-of-month
calculations. This time structure template is defined in the file time.h.


RELATIONSHIPS:
                                ┌──────────┐
                        ┌─────►          ├──────────────────────────┐
                        │     │ ctime()  │                          │
                        │     └──────────┘                          │
                        │                                           │
┌────────┐ ┌──────┐ ┌───┴───┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─▼──────┐
│▒system▒│ │      │ │▒▒▒▒▒▒▒├─►         ├─►▒▒▒▒▒▒▒▒▒├─►         ├─►▒▒▒▒▒▒▒▒│
│▒▒time▒▒│ │time()│ │▒long▒▒│ │gmtime() │ │struct tm│ │asctime()│ │ string │
└────────┘ └──────┘ └───┬───┘ └─────────┘ └────────┘ └─────────┘ └────────┘
                        │                       │
                        │     ┌───────────┐     │
                        │     │           │     │
                        └─────►localtime()├─────┘
                                └───────────┘


════════════════════════════════════════════════════════════════════════════
                                ┌──┐
LEGEND:    ▒▒▒ = data type    │  │
            ▒▒▒                └──┘ = ctime package function

        FIGURE 4-1 ▐  Time and date functions and variables


        Time and date information in the structure is easy to access and use,
but it is still not suitable for human consumption. Both gmtime() and
localtime() return pointers to a time structure. The declarations for these
functions are

        #include <time.h>
        struct tm *gmtime(time);
        long *time;
        struct tm *localtime(time);
        long *time;


                                CAUTION
            These two functions use a single statically allocated
            structure. Any call to one of these functions,
            therefore, will overwrite the result of any previous
            call to either function.

        While gmtime() always returns a direct conversion of the time value
returned by time(), localtime() does some additional adjustments to
compensate for the difference between GMT and the time zone set into the
DOS environment via the TZ variable. If TZ is not explicitly set (it
usually isn't), then Pacific time is used. I am willing to wager that
nearly every PC running DOS today thinks it is in Redmond, Washington!
We'll see shortly how you can tell your PC where it really is, but
first we need to examine how to convert the time structure data into
strings we can read and understand.

        Creating Strings  The function asctime() takes a pointer to the time
structure and converts the structure members to a 26-byte string. The
format of the string is

        Sat Jul 12 13:05:33 1986\n\0

where \n is a newline character and \0 is the terminating null byte. Each
character should be surrounded by single quotes to show that it is a
character constant. Rather than clutter the displayed string, I'll just ask
you to imagine the quotes there.
        The function ctime() provides a shortcut for creating a local time
string. It takes a long time result from time() and returns a pointer to a
character string of the format described above. Its declaration is

        #include <time.h>
        char *ctime(time);
        long *time;

                                CAUTION
            The character string pointed to by ctime() is the same
            statically allocated string pointed to by asctime(). A
            call to one of these routines destroys the result of
            any previous call to either of them.

        Time Zones  Now on to the subject of time zones. A DOS environment
variable called TZ can be set to reflect the time zone in which a PC is
operating. Adding a line like

        set TZ=MST7MDT


to my AUTOEXEC.BAT file tells DOS that my machine is running in the
mountain time zone, that there is a 7-hour difference between mountain
standard time and Greenwich mean time, and that daylight saving time is
honored in this zone. Several other variables are also maintained by the
ctime() subroutine package. They can all be updated by a call to tzset()
(or to asctime(), which calls tzset()). These variables are

═══════════════════════════════════════════════════════════════════════════
    VARIABLE                 MEANING
───────────────────────────────────────────────────────────────────────────
    int daylight;         │  Daylight savings time flag
    long timezone;        │  Difference from GMT in seconds
    char *tzname[];       │  timezone strings
────────────────────────┴──────────────────────────────────────────────────

If a daylight string is set in the TZ variable, daylight is set to a
nonzero value. The value of timezone is the difference in seconds between
local standard time and GMT. The variable tzname is an array of pointers to
character strings. The first (tzname[0]) is a pointer to the three-letter
string for the standard time zone and the second (tzname[1]) is a pointer
to the daylight string (e.g., MDT) if one was specified. The last element
(tzname[2]) is a NULL pointer that typically terminates an array of
pointers. If no TZ setting is done at boot time, the ctime package uses
PST8PDT as a default.
        When compiled and run, the following little program, TIMEDATA, shows
some of the time-related values for your system. Don't be surprised to see
Pacific time zone data even if that is not your "home" time zone.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 49 in the printed version of the book.   ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        Here is an example of the output of TIMEDATA taken from my system in
Denver, Colorado:


daylight savings time flag = 1
difference (in seconds) from GMT = 25200
standard time zone string is mst
daylight time zone string is mdt

ctime():        Mon Feb 16 06:19:33 1987

local time:     Mon Feb 16 06:19:33 1987

universal time: Mon Feb 16 13:19:33 1987


        If you accept the Pacific default for timezone while operating in a
different time zone, there is usually no harm done. The effect of not
setting TZ is that you are fibbing about the local time, which will cause
DOS to have an incorrect notion of what GMT is. For example, had I let TZ
default to PST8PDT and set local time correctly for Denver, the system
value for GMT would be off by one hour.
        However, problems might occur for systems connected into wide-area
networks that must keep accurate track of file-modification times and
various other timed events, possibly while crossing several time zones. In
such cases, all connected systems should have the correct time zones set in
the TZ variable and agree on what the value of GMT is.
        Getting a time-and-date string is a first step for us in the
development of a program called NOTES, a simple electronic diary. But we
will also need a way of creating and modifying files before we can assemble
a working prototype, so let's talk about that next.


Basic File and Character I/O Functions

        Much of what we will do in the rest of this book will involve some
interaction with disk files. There are many standard I/O routines in the C
run-time library that deal with files on various levels. In this section,
we will review some of the more frequently used functions. Then we will
create a useful demonstration program.
        The run-time library provides access to disk files and other
I/Ostreams using DOS file handles. These functions--open(), close(),
read(), write(), lseek(), and so on--are analogous to the UNIX system calls
of the same names that use file descriptors. These functions are unbuffered
from a user's perspective, although the operating-system kernel may
anticipate read operations and delay write operations to minimize disk-head
movement, and improve I/O efficiency.


        Buffered I/O

        Another level of file access is the set of standard library
subroutines that do buffered I/O. The functions fopen), fclose(), fgetc(),
fputc(), fgets(), and fputs() are representative of this level, and are
used extensively in the programs in this book. These functions use a file
pointer that points to a structure of type FILE. The FILE structure
template is defined in stdio.h, the header file that is included in all
source files that use standard I/O library functions.
        All file-access functions will be covered at one time or another in
programs later in the book. For now, we'll concentrate on the buffered I/O
library functions. By way of example, here is a fairly typical use of the
standard run-time library in a program called NOTES, which lets us keep an
electronic notebook or diary. This first version, NOTES1, is a simplified
prototype.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 52 in the printed version of the book.   ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        This program, although rather simple and not yet fully developed, is
immediately useful in its current form. It takes input from the keyboard
and appends it to a "notesfile" in the current directory. The program is
invoked by typing NOTES on the DOS command line. Each new session starts by
placing a blank line and a date/time stamp at the end of the data file,
currently hard-coded into the program as NOTES.TXT. Text input is simply
typed at the keyboard and terminated by the user typing a Ctrl-Z (end-of-
file) character on a line by itself.
        This simple version of NOTES allows no options. If you make an error
while inputting text, you must back up and correct it before moving off the
line. To edit the NOTES.TXT file, you must leave the NOTES program and call
upon some text-editing program. We will embellish the NOTES program with
additional features in later chapters, but for now we will accept its
limitations and treat it as a learning aid.
        The call to fopen() is interesting. The function will open the named
file for appending if the file already exists, or it will silently create
the file if it doesn't exist. The call returns a valid FILE pointer if the
file is opened successfully, or NULL to flag an error. (Attempting to open
a directory, for example, produces an error.) Our program must be prepared
to deal with errors. Therefore, at the risk of having our programs look
like LISP source code, we will always test the return value of I/O
functions. To ignore this extra bit of work is not simply bad programming
practice; it can be dangerous. A program that fails without a whimper is
not apt to inspire strong feelings of trust in its users.
        The declaration of fopen() is

        #include <stdio.h>
        FILE *fopen(pathname, type);
        char *pathname;
        char *type;

        The pathname argument may be a simple filename and optional extension,
or a full or relative pathname with an optional leading drive specifier.
The type argument is one of the following:

═══════════════════════════════════════════════════════════════════════════
    ARGUMENT       MEANING
───────────────────────────────────────────────────────────────────────────
    r           │  Open for reading (file must exist)
    w           │  Open for writing (create or truncate)
    a           │  Open for appending (create if necessary)
    r+          │  Open for both reading and writing (the file must exist)
    w+          │  Open for both reading and writing (create or truncate)
    a+          │  Open for reading and appending (create if necessary)
───────────────────────────────────────────────────────────────────────────

The optional + within the type string specifies update mode, in which both
reading and writing are allowed. The file pointer must be set by an
intervening call to fseek() or rewind() when switching between read and
write operations or vice versa.
        Unlike UNIX and XENIX, which use a single newline (NL) character to
terminate a text line, DOS uses a carriage-return character (CR) plus a
linefeed (LF) character. The codes to NL and LF are identical (ASCII code
10), so you will usually see the DOS end-of-line treatment described as
CR/NL, but it is really CR/LF. Technically, LF moves down a line but
retains the same column position, so a CR is needed to get back to the
first column. The NL code convention achieves the two tasks in a single
operation, saving a byte per line in ordinary text files. Both methods of
terminating a line are allowed by the ANSI standard (see Chapter 13 for
more information about ANSI standards).
        In addition to a different end-of-line treatment, DOS uses an explicit
Ctrl-Z (ASCII code 26) to mark the end of a file. (Under UNIX and XENIX,
the end-of-file condition is sensed when an attempt is made to perform an
operation beyond the last position in a file, which is known because an
exact size is maintained for each disk file.)
        The conventions just described for DOS text files do not apply to
binary files (executable program files, for example), which can contain any
arbitrary bit patterns in any position. For binary files, all bits are
significant, and an accurate file size must be maintained by DOS so it
knows where the end of the file is.
        To maintain compatibility with existing C programs, UNIX textfile
conventions are used within C programs under DOS. When a file is opened,
the differences are specified by a text (t) or binary (b) flag appended to
the type argument to fopen(), or by the global _fmode translation-mode
variable if neither t nor b is explicitly specified. The default for _fmode
is text-translation mode.
        We can use separate statements for the processing and the
errordetection phases of each call to a library routine:

FILE *fp;

fp = fopen(notesfile, "a");
if (fp == NULL)
        fatal(pgm, notesfile, 1);

This is rarely done, however. You are more likely to see this written in
the condensed form (shown in notes1.c, on page 52) which is the de facto
convention for embedding assignments that is used by many, but by no means
all, C programmers:

        FILE *fp;

        if ((fp = fopen(notesfile, "a")) == NULL)
                fatal(pgm, notesfile, 1);

Don't let "convention" sway you, however. Use whichever form looks pleasing
to your eye. Just be sure the statements are logically correct and that
error conditions are checked and responded to in a reasonable way.
        The getchar() and putc() calls are actually macros defined in stdio.h.
These calls tend to work somewhat faster than the equivalent functions,
fgetchar() and fputc(), because they expand to in-line code and eliminate
the function-call overhead associated with true functions. However, if we
had many calls to putc() and getchar() (which is just getc() with stdin
specified as the stream), we would save some space by using the functions
because the code for the function only has to appear one time in the object
file, rather than once for each call to the macros.
        Rather than clutter this chapter with information that is available to
you in the C compiler's Library Reference Manual, we'll just refer to that
document (or the equivalent for other C compilers) for more details and
move on to some other interesting topics that are essential to the
development of our programs.


Process Control Functions

        A process is an environment in which a program executes. When you run
the NOTES.EXE program, for example, DOS allocates an instruction segment, a
user data segment, and a system data segment. It then initializes the
instruction and data segments from the program and starts execution.
        The Microsoft C run-time library provides a group of functions for
controlling the execution of processes from within a process. The easiest
to use is system(), which takes a program-name argument (may be a full or
relative pathname) and runs the named program as a subprocess. When the
subprocess terminates, control is returned to the calling process. The
declaration of system() is

        #include <process.h>
        int system(string);
        char *string;

where the string argument is the name of a built-in DOS command or an
external program that must be in the current directory or be accessible
via the PATH variable. DOS must be able to find and load COMMAND.COM to
service the system() call. A return of -1 indicates an error.
        A simple example of the system() function in use is the following code
fragment that calls a text editor from within a running process:

                .
                .
                .
        if (system("EDIX") == -1) {
                perror("System error");
                /* error recovery procedure */
                        .
                        .
                        .
        }

        An error return could be caused by the absence of a valid COMMAND.COM
file or by the inability of DOS to find the EDIX.EXE file with the
information provided by the system() call plus the current value of
PATH.
        The exec() and spawn() families of process-control functions are more
complicated to use, but they provide considerably more versatility. The two
families have much in common and each has six members. The base function
name--exec, for example--takes a one- or two-letter suffix to fully specify
the function's behavior. Thus, the exec() function names are execl(),
execle(), execlp(), execv(), execvp(), and execve(). The symbols appended
to the base function names have these meanings:

═══════════════════════════════════════════════════════════════════════════
    SUFFIX         MEANING
───────────────────────────────────────────────────────────────────────────
    l           │  Uses a NULL-terminated list of arguments
    v           │  Uses a variable list of arguments
    e           │  Receives an environment pointer
    p           │  Searches PATH to find program files
───────────────────────────────────────────────────────────────────────────

        Rather than show all six function declarations for each of the
families, we'll look at one representative function of each. Both of these
functions automatically pass along a copy of the current environment to the
child process. Here is the declaration for execl():

        #include <process.h>
        int execl(pathname, arg0, arg1, ..., argn, NULL);
        char *pathname;
        char *arg0, *arg1, . . .,*argn;

        The declaration for spawnvp() is

        #include <process.h>
        int spawnvp(modeflag, pathname, argv);
        int modeflag;
        char *pathname;
        char *argv[];

        The exec() family always overlays the current process (parent) with a
new process (child) that destroys the parent, so it is impossible to return
from an exec() function call. The spawn() family, however, runs the
subprocess with an action determined by the modeflag parameter. A value of
P_WAIT causes the parent process to suspend execution while the child runs.
When the child process terminates, control is returned to the parent. A
spawn() call with a modeflag value of P_OVERLAY produces the same effect as
the equivalent exec() function call.
        Arguments are handled differently for the fixed-list (l) and variable-
list (v) versions of these functions. The fixed-list version is used when
we know in advance how many arguments will be presented. We place a NULL
argument after the last argument we want to pass, to tell the exec() or
spawn() function that the list is complete. In the variable-list version of
the functions, a pointer to an argument list is passed. The argument list
is handled just like the argv of a program's main() function, which was
described earlier in this section.
        The first argument in a fixed list (arg0) or a variable list (argv[0])
is usually a copy of the pathname argument. Other values will not cause an
error, but NULL should not be used in the first position of a fixed list:
The NULL would effectively hide the remainder of the list from the child
process, since a NULL argument marks the end of the list. Recall that under
DOS version 3.00 and later, the program name is available to the child as
arg0 or argv[0].
        Here is an example of the spawnvp() function in action. It is used to
run a text editor as a subprocess. If an EDITOR environment variable is
defined, its string value is used as the program name. If EDITOR is not
defined, NOTES uses the DOS EDLIN program. The editor is given the name of
the current notesfile as an argument. This update to the NOTES program also
adds an alternative way of terminating input (using a dot alone on a line
like the UNIX mail program) and a way of ignoring the Ctrl-Break interrupt
so that a user cannot garble the NOTES.TXT file by interrupting it in the
middle of text entry.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 58 in the printed version of the book.   ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        The source listing for NOTES contains a few things we haven't shown
before. First, the signal() function is used to disable the Ctrl-Break
input from the keyboard. Since signal() returns a pointer value of -1 on
errors instead of the usual NULL, we need to use the strange-looking cast
of the value on the right side of the comparison. The (int(*)())-1 is
needed because the C compiler expects a pointer to an integer.
        Second, the dot (.) is used to terminate data input. This involves a
slight complication. A dot usually does not occur in free text as the first
character of a line, but it may occur anywhere else. We depend on this, and
check that the current position is the beginning of a line and that the
input character is a dot before terminating data entry.
        Third, the EOF signal (a typed Ctrl-Z) may be used anywhere to halt
data entry. It should, however, be used only on a line by itself. If Ctrl-Z
is typed following some text on a line, some programs will not accept the
line because it is not properly terminated. Programs should be written to
handle this situation, but many insist that each line in a text file end
with a CR/LF combination and may fail in unpredictable ways if a line dead-
ends without the expected termination.
        The next program, RUN_ONCE, is a bit of a novelty, but it has a
purpose in some restricted circumstances. Let's say you have an office
setting in which computer users with very limited experience are expected
to run a single program, perhaps an integrated database-spreadsheet-word
processor-blender-and-kitchen-sink program. You want the system to come up
running this program and you also want to prevent the users from exiting to
DOS. If they quit the catchall program, the RUN_ONCE program tells them to
turn off the power and then it locks up the machine.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 62 in the printed version of the book.   ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        After compiling this program, try running it on your favorite text
editor or some other program. For example,

        run_once word myfile.doc

loads and executes the RUN_ONCE program, which spawns Microsoft Word with
MYFILE.DOC as an argument. When the user quits Word, the system displays
the exit message and appears to be dead. A reboot is required to restart
the system. If the RUN_ONCE program is embedded in the AUTOEXEC.BAT file,
it will prevent the users from doing anything except what is planned for
them by whoever sets up the system. (This is easily defeated by someone who
knows DOS. All that is required is to load a floppy disk with another copy
of DOS that does not have the RUN_ONCE program installed. But someone who
knows this probably knows how to use DOS anyway.)


Other Library Functions

        We have barely scratched the surface of the standard run-time library
provided with the Microsoft C compiler. Our purpose has been to describe
some representative library routines and to illustrate their use in real
programs. As we develop programmer's utilities and other programs in the
remainder of this book, we will introduce more of these routines in
context.
        The functions and macros we have looked at so far are portable, with
little or no change, to various versions of DOS and to UNIX-based operating
systems. The next chapter delves into functions that are tied intimately to
DOS and are therefore not easily ported to other operating environments.
Although they lack portability, the functions are important for programs
that need to obtain the highest level of performance possible from a PC.



Chapter 5  PC Operating System Interfaces



        For me, one of the joys of working with a personal computer, after
having spent years working on time-sharing systems over slow-speed data
lines, is the incredible responsiveness of the PC in terms of screen
updates. To "paint" a 24-line screen at 9600 bps (bits per second)
typically takes about 4 seconds--not too bad, really. But at the more
common 1200 bps used by most dial-up connections, a complete paint job
takes closer to 16 seconds. Paging through a document or a source list at
this speed becomes a bit of a chore, and one quickly learns how to use
regular expression searches, small window sizes, and other tricks to
minimize the time it takes to find the material of interest.
        In contrast, using one of the slowest methods available on a PC, the
DOS print-character function call, painting a full 25-line screen takes
only about 4 seconds. By using BIOS routines intelligently, one often can
do the job in less than half a second. Using direct screen access, the
updates become virtually instantaneous. The PC has spoiled me, and it is
really hard to go back to dial-up operations: It feels like the whole world
has been put into slow motion.
        In this chapter, we'll explore methods of managing screen displays via
the DOS and BIOS interrupt services. The methods used here are not portable
to UNIX and XENIX, and the BIOS routines are not even guaranteed to be
portable to some nominally IBM-compatible hardware.
        This material is presented to acquaint you with some of the techniques
used in commercial-quality programs. The mark of such programs is how well
they serve the user's needs. Among the needs I hear most often expressed,
the following are related to screen and system configuration:

        ►  The program must work on any compatible machine, regardless of the
        configuration (disks, display type, and so forth).

        ►  The screen must be updated quickly_no growing old waiting around
        for the next frame.

        ►  If both color/graphics and monochrome display systems are
        installed, it must be possible to choose which to use.

        ►  There must be no flickering, blizzard effect, or other visual
        "noise" at any time.

        Using DOS and BIOS routines, we can satisfy these needs almost
completely. Only the first one is a problem because some machines were
designed in a way that limits their BIOS compatibility. As a developer, I
want to maximize the audience, so I address such machines by using the ANSI
device driver, which is covered in Chapter 13.
        The routines in this chapter cover needed functions that are not
already handled by the standard run-time library, with a few exceptions. In
those few cases, a function is presented that duplicates a system function
because that function may not be available in the library of other C
compilers. A case in point is kbhit(): The function keyready() performs the
same function for those whose libraries don't have kbhit() available.
        We begin with an overview of the DOS and BIOS access routines in the
standard library. These routines are the low-level access functions upon
which our interface routines are built. Then we will write a program that
shows how to use many of the routines we develop. The routines presented
here are not a complete set; they are the high-use routines that we will
need shortly. Other routines will be added to the DOS and BIOS libraries as
we need them in later chapters.


System Interface Functions

        The C run-time library provided with Microsoft C contains several low-
level system-access functions. We will use three of them as a basis for our
DOS and BIOS interfaces: bdos(), int86(), and intdos().
        The functions int86x() and intdosx() are used in programs that use
long addresses. They behave just like their int86() and intdos()
counterparts, but they take an additional argument that specifies segment
registers used in forming long addresses. The segment register values can
be obtained by using the segread() function.
        The file dos.h in the \include directory contains several structure
and union definitions that are needed by the system access functions. If
you are not comfortable using C unions, here is a good example of how they
may be used.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 67 in the printed version of the book.   ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        The header file defines two structures for the CPU registers and
flags. The first is struct WORDREGS:

    /* word registers */

    struct WORDREGS {
            unsigned int ax;
            unsigned int bx;
            unsigned int cx;
            unsigned int dx;
            unsigned int si;
            unsigned int di;
            unsigned int cflag;
            };

        The second structure is struct BYTEREGS:

    /* byte registers */

    struct BYTEREGS {
            unsigned char al, ah;
            unsigned char bl, bh;
            unsigned char cl, ch;
            unsigned char dl, dh;
            };

        A composite word/byte register data structure is made by overlaying
the two structures, like this:

    /* general purpose registers union - overlays the corresponding word and
    * byte registers.
    */

    union REGS {
            struct WORDREGS x;
            struct BYTEREGS h;
            };

        The union REGS lets us access the registers either as bytes (AL, AH,
and so on) or as words (AX, BX, and so on). We can also access the system
carry flag as a word register. It is unfortunate that we don't have similar
access to the zero flag.
        Now that we have the data structures in mind (or at least on paper),
we can examine the primary system-access routines. Each is presented here
with a manual-page summary and a brief description. In the next two
sections, we will apply these functions to the task of creating our DOS and
BIOS libraries. Any file that calls the following functions must include
the header file dos.h by using the line

        #include <dos.h>

        Here are the system-level functions:

    int bdos(dosfn, dosdx, dosal);
    int dosfn;                      /* function number */
    unsigned int dosdx;             /* DX register value */
    unsigned int dosal;             /* AL register value */

        The bdos() function is a DOS function interface that loads the DX and
AL registers with values provided by the caller and then executes an  INT
21H. Following the DOS function call, the value of the AX register is
returned to the caller.
        The uses of bdos() are limited to DOS function calls that take
arguments in either or both of the DX and AL registers (or none at all) and
that do not use the carry flag to indicate errors. To minimize function-
call overhead, we will use bdos() in some keyboard functions and recommend
its use for macros or for in-line code within a program.

    int int86(intno, inregs, outregs);
    int intno;                      /* interrupt number */
    union REGS *inregs;             /* register values on call */
    union REGS *outregs;            /* register values on return */

        The int86() function is a general-purpose interface routine that gives
us nearly complete access to the full range of software interrupts. The
function sets up the registers according to the caller's wishes and
executes the specified interrupt. Upon return from the function call,
int86() fills outregs with the current register values and the value of the
system carry flag, which will have a nonzero value if an error has
occurred.
        We will use int86() to call BIOS routines for video and keyboard
access and to check equipment and memory information. We also could use it
to access DOS functions, but intdos() does the job faster.

    int intdos(inregs, outregs);
    union REGS *inregs;             /* register values on call */
    union REGS *outregs;            /* registers values on return */

        The intdos() function is just like the int86() function except that it
is designed to make DOS function calls directly and does not require an
interrupt number. The INT 21H interrupt number is effectively hard-coded
into intdos(). The same input and output register behavior is observed as
for int86().
        The declaration of the register arguments to int86() and intdos()
shows them as pointers. The union and structure definitions in dos.h do not
reserve storage; they simply describe the needed storage requirements. We
must declare storage in our routines by a statement like

        union REGS inregs, outregs;

This statement allocates automatic storage for the unions. We then can load
values into inregs, make the needed function call, and extract data from
outregs. (There is nothing sacred about the names inregs and outregs. Use
anything that seems appropriate, but ideally the names should be
descriptive.) The int86() and intdos() functions need the addresses of the
unions. A call to intdos() looks like this:

        intdos(&inregs, &outregs);

        We can access elements of the structures either as bytes or as words.
To load a value into AL, for example, we use the byte-oriented
approach:

        inregs.h.al = value;

To read the results of a function from the DX register, we use the word-
oriented approach:

        value = outregs.x.dx;

Next, we will put these system-level library functions to work for us in
two important libraries in our growing collection of function
libraries.


DOS Library Routines

        The DOS library is composed of only a few routines that supplement the
standard run-time library, which is very complete in its coverage of disk,
keyboard, date/time, and many other functions. The routines presented here
are primarily for the benefit of those who are using C compilers with
libraries that do not fully support the DOS interface.
        A local header file, doslib.h, contains constants used to specify DOS
interrupts and the function numbers for calls to INT 21H, the DOS
"umbrella" interrupt.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 72 in the printed version of the book.   ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        DOS Version Number

        Under Microsoft C, the header file stdlib.h defines the global
variables _osmajor and _osminor. These variables receive the DOS major and
minor version numbers when a program starts running. The numbers may be
used to verify that the version of DOS is capable of supporting features
required by a program. For example, programs that use pathnames require DOS
version 2.00 or later. The simple block of code added to the program
provides low-cost insurance and alerts the user that it's time to upgrade.

    if (_osmajor < 2) {
            fprintf(stderr, "Need DOS 2.00 or later\n");
            exit(1);
    }

        Another way to get the version number is to ask DOS for the number.
The function ver() does the job.

    /*      ver -- get the MS-DOS (or PC-DOS) version number   */

    #include <local\doslib.h>

    /************************************************************
    *  For MS-DOS versions prior to 2.00, the low byte (AL) of
    *  the return value is zero.  For versions 2.00 and beyond,
    *  the low byte is the major version number and the high
    *  byte (AH) is the minor version number.
    ************************************************************/

    int ver()
    {
            return(bdos(DOS_VERSION, 0, 0));
    }

        The ver() function uses bdos() to get the version number from DOS. It
returns the return value from bdos(), which is the value in the AX
register. AL holds the major version number and AH holds the minor version
number. For versions of DOS prior to version 2.00, the returned major
number is 0.


        Keyboard Functions

        The run-time library includes a couple of functions that read from and
test the keyboard buffer. This section presents some alternative functions
that do essentially the same jobs and which can easily be modified for use
with other compilers.
        To gather the user's input, we can call the run-time library function
getch(). It reads the next available character from the keyboard buffer and
responds to a Ctrl-Break. If there is nothing ready to read, it waits until
there is. This operation is called a "blocking read" because the machine
simply marks time while waiting for the user to type something. If the user
presses Ctrl-Break, getch() executes the Ctrl-Break handler.
        The function getkey() also does a blocking read. However, it differs
from getch() in a few ways. First, it ignores Ctrl-Break. This prevents a
user from breaking out of a program at a critical point that might damage
files or data stored in memory. Second, getkey() checks the value of the
keyboard code. If the code is a null byte (\0), it gets the next character
code, bitwise-ORs it with 0x100 (which has the same effect as adding 256),
and returns the modified value. This value can be compared against the
constants that are defined in keydefs.h to determine what key was pressed.
The defined values include most of the keys and combinations of keys on the
keyboard.
        Here is the source code for keydefs.h:


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 76 in the printed version of the book.   ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        This is the source code for getkey():

    /*      getkey -- return a code for single and combo keystrokes
    *      - returns a unique code for each keystroke or combination
    *      - ignores "Ctrl-Break" input     */

    #include <dos.h>
    #include <local\std.h>
    #include <local\doslib.h>
    #include <local\keydefs.h>

    int getkey()
    {
            int ch;

            /* normal key codes */
            if ((ch = bdos(KEYIN, 0, 0) & LOBYTE) != '\0')
                    return (ch);

            /* convert scan codes to unique internal codes */
            return ((bdos(KEYIN, 0, 0) & LOBYTE) | XF);
    }

        To determine whether a key has been pressed, we would use the run-time
library function kbhit(), which returns a nonzero value if a code is
available in the keyboard buffer, indicating that at least one key has been
pressed. The function keyready(), presented next, does the same job.

    /*      keyready -- nonzero if the keyboard buffer
    *      has any codes waiting   */

    #include <dos.h>
    #include <local\doslib.h>

    int keyready()
    {
            union REGS inregs, outregs;

            inregs.h.ah = CH_READY;
            intdos(&inregs, &outregs);
            return (outregs.h.al);
    }

        The kbhit() or keyready() function should be called before an attempt
is made to read the keyboard. This produces a "nonblocking read." If
nothing is ready, the program can do something useful while it waits for
the user to press a key. That something should be broken up into brief
allocations of work (i.e., sending a character to the printer), so that the
keyboard provides a timely response to user input. There is, however, no
good reason to put the entire computer to sleep waiting for user input. The
following code fragment shows a way to handle keyboard processing and a
background task:

    .
    .
    .
    int k, getkey(), keyready();    /* keyboard functions */
    int do_task(TASK);              /* task dispatcher */
    TASK taskp;                     /* task pointer */
    .
    .
    .
    while (1) {
            /* get user's input */
            if (keyready()) {
                    k = getkey();
                    switch (k) {
                    case K_ESC:
                            do_exit();
                    .
                    .
                    .
                    }
            /* do background task */
            do_task(taskp);
            }
    .
    .
    .

        For example, if the task dispatcher is calling a routine that dumps
characters into a printer spooling buffer, the printer appears to run
continuously while not burdening the main program at all. User commands and
input will be honored immediately. Other background tasks might include
doing a hard disk-to-tape backup, checking timed alarms, and running a
sprinkler system.

        The makefile, dos.mk, automatically builds the library dos.lib:

# makefile for the DOS library
LINC = c:\include\local
LLIB = c:\lib\local

# --- inference rules ---
.c.obj:
        msc $*;

# --- objects ---
OBJS = getkey.obj keyready.obj ver.obj

# --- compile sources ---
getkey.obj:     getkey.c $(LINC)\std.h $(LINC)\doslib.h $(LINC)\keydefs.h


keyready.obj:  keyready.c $(LINC)\doslib.h


ver.obj:       ver.c $(LINC)\doslib.h


# --- create and install the library ---
$(LLIB)\dos.lib: $(OBJS)
        del $(LLIB)\dos.lib
        lib $(LLIB)\dos +$(OBJS);


BIOS Library Routines

        The ROM BIOS in a PC offers a wide range of services that we can use
to our advantage. We will write routines that let us determine how the host
computer is configured, control the cursor, check the status of special
keyboard keys (such as Caps Lock and Num Lock), and read and write
characters and video attributes anywhere on the screen.
        The header file bioslib.h contains the constants needed to call
various BIOS interrupts and services by symbolic names.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 82 in the printed version of the book.   ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        Equipment Determination Functions

        We'll start with a pair of functions that help us determine what
equipment is installed in a PC and how much memory it has.
        The first of these functions, equipchk(), uses BIOS interrupt 11H to
determine what devices are installed. A structure is used to hold the data
obtained by equipchk(), which declares the structure globally and fills it
when called. Other functions can also access the data if they contain the
following external definition:

        extern struct EQUIP Eq;

        Here is the source code for equip.h and equipchk().

    /*      equip.h -- header for equipment determination/inventory   */

    struct EQUIP {
            short   disksys,        /* 1 if disks installed */
                    game_io,        /* 1 if game i/o adapter installed */
                    nprint,         /* number of printer ports */
                    nrs232,         /* number of serial ports */
                    vmode,          /* initial video mode (from switches) */
                    ndrive,         /* number of disk drives
                                        (from switches) */
                    basemem;        /* amount of base memory in Kbytes */
    };


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 84 in the printed version of the book.   ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        To find out whether a game port is installed, call equipchk() and then
look at the game_io member of the structure.

    #include <local\equip.h>
    extern struct EQUIP Eq;
            .
            .
            .
    if (equipchk())
            /* handle the error */;
    if (Eq.game_io == 1)
            printf("Game adapter installed\n");

        Although equipchk() can tell us how much memory is installed on the
main system board, it cannot help us get the total installed memory value.
The memsize() function calls BIOS interrupt 12H to get the system memory
size, including any memory in the I/O channel on adapter cards (exclusive
of display memory and that which is not contiguous with the main system-
board memory).

    /*      memsize -- get memory size   */

    #include <dos.h>
    #include <local\std.h>
    #include <local\bioslib.h>

    int memsize()
    {
            union REGS inregs, outregs;

            return (int86(MEM_SIZE, &inregs, &outregs));
    }

        The size of main memory is reported as a 16-bit integer that
represents the total number of kilobytes found.


        Keyboard Status

        Older PC keyboards did not have status lights on the Caps Lock and Num
Lock keys. It is helpful to the user if the status of these keys is
reported on the screen in applications in which the status is important.
The same holds true for the Ins key and, in some situations, the Scroll
Lock key. Also, we can use the status of the Shift, Ctrl, and Alt keys to
alter the meaning of the function keys to do other jobs.
        The kbd--stat() function lets us obtain the needed information. It
calls BIOS interrupt 16H, service 2 (status), and returns the value of the
AL register. The header file keybdlib.h contains definitions of masks that
are used to test the return value from kbd_stat() to determine which keys
were pressed.

    /*      keybdlib.h   */

    #define KBD_READ        0       /* keyboard routine numbers */
    #define KBD_READY       1
    #define KBD_STATUS      2

    #define KS_RSHIFT       0x0001  /* bit masks for keys and states */
    #define KS_LSHIFT       0x0002
    #define KS_CONTROL      0x0004
    #define KS_ALT          0x0008
    #define KS_SL_STATE     0x0010
    #define KS_NL_STATE     0x0020
    #define KS_CL_STATE     0x0040
    #define KS_INS_STATE    0x0080



    /*      kbd_stat -- return the keyboard status
    *      word (bit-significant)   */

    #include <dos.h>
    #include <local\bioslib.h>
    #include <local\keybdlib.h>

    unsigned char kbd_stat()
    {
            union REGS inregs, outregs;

            inregs.h.ah = KBD_STATUS;
            int86(KEYBD_IO, &inregs, &outregs);
            return ((unsigned char)(outregs.h.al));
    }


        Video Access

        One of the most frequently used ROM BIOS interrupts is 10H, the access
point for video services. We will use many of these services in our
programs.
        The header file video.h contains some data structures used to hold
important video-mode and cursor data. It also contains definitions of color
and attribute values, special and drawing-character codes, and constants
used by the mode and cursor structures.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 87 in the printed version of the book.   ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        The primary functions in interrupt 10H are those used to get the
current video state data (mode, screen width, and visual page) and to set
the video mode.
        The getstate() function invokes video service 15 to get the values of
the video mode, the screen width (redundant information, because the mode
number implies a width, but screen width is directly accessible), and the
visual page number. Although the IBM Technical Reference manual calls this
the active page, it is really the visual page. To be consistent with the
way BASIC defines pages, the active page is defined as the one being
written to and the visual page as the one being viewed. The page number
returned by service 15 is the visual page. Most of the time, the visual and
active page numbers are the same.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 91 in the printed version of the book.   ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        Because getstate() contains the declarations of the video structures,
it should be called before you use any of the other video functions
(clrscrn(), clrw(), scroll(), and setctype()) that depend on the data
in those structures.
        The setvmode() function is used to set the video mode. It cannnot be
used, however, to switch from one adapter to another. That task must be
handled in another way, which is described in Chapter 11. The mode numbers
are defined in video.h. The mode number presented to setvmode() is not
value-checked.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 92 in the printed version of the book.   ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        For the IBM Monochrome Adapter, and for the standard Color Graphics
Adapter (CGA) in any of its graphics modes, only page 0 is valid. On the
CGA, the 40-column text modes permit up to eight screen pages in display
memory and the 80-column text modes up to four screen pages. On the
Enhanced Graphics Adapter (EGA), page ranges are a function of the mode as
follows: mode 13, 0 through 7; mode 14, 0 through 3; modes 15 and 16, 0 and
1.
        The job of setting the visual page falls to setpage(). The pagenumber
argument must be valid for the current video mode because its value is not
checked.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 94 in the printed version of the book.   ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        Next we have four functions that are used for cursor control. Two of
them, readcur() and putcur(), deal with cursor positioning. The other two,
getctype() and setctype(), are used to determine the cursor shape and to
set it to a particular shape, respectively.
        The function readcur() gets the cursor position for the specified page
and passes back the row and column position data to the calling
routine.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 94 in the printed version of the book.   ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        The row and col arguments are pointers to simulate "call-by-reference"
parameter passing, which allows readcur() to pass back more than a single
value by directly accessing the storage locations of the variables in the
calling function.
        The putcur() function moves the cursor to an absolute screen location
on the specified screen page. The specified row, column, and screen-page
values are not checked.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 95 in the printed version of the book.   ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        The cursor on a PC screen is a "hardware cursor" formed by one or
more raster scan lines in the cell at the current cursor position. The
cursor always blinks. A hardware cursor is available only in text modes and
not in graphics modes, although it is quite easy to fabricate one in
software.
        To determine the starting and ending scan lines for the cursor, we use
getctype(). The function uses the same video service as readcur() but
returns cursor type data, not position data. We can use getctype() in a
program to save the starting and ending scan lines so that we can restore
them with setctype() before the program terminates.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 96 in the printed version of the book.   ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        The inverse operation, setting the cursor type, is a bit more
difficult than finding out what type it is. The job is done by setctype().
The setctype() function sets the starting and ending scan lines to the
values given by its arguments. It is incumbent upon the calling function to
specify correct values for the prevailing display mode.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 97 in the printed version of the book.   ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        The video functions described next are used to read and write
characters and attributes in any video mode, and dots--individual pixel
values--in graphics modes only.
        The function readca() gets the character and video attribute of the
character cell at the cursor position without regard for the video mode.
This is a neat trick in graphics modes when you realize that, to do this,
the BIOS video routine has to do a pattern-matching operation on the
displayed image to find out what the character is.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 98 in the printed version of the book.   ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        When we are operating in one of the graphics modes, we can read the
values of individual picture elements--pixels, or dots. The function
readdot() returns the color number for the pixel at the specified row and
column coordinates.

    /*      readdot -- read the value of a pixel (in graphics mode only) */

    #include <dos.h>
    #include <local\std.h>
    #include <local\bioslib.h>

    int readdot(row, col, dcolor)
    int row, col;
    int *dcolor;   /* pointer to dot color */
    {
            union REGS inregs, outregs;

            inregs.h.ah = READ_DOT;
            inregs.x.cx = col;
            inregs.x.dx = row;
            int86(VIDEO_IO, &inregs, &outregs);

            *dcolor = outregs.h.al;

            return (outregs.x.cflag);
    }

        The allowed row and column values depend on the graphics mode:

═══════════════════════════════════════════════════════════════════════════
    MODE            ROW RANGE           COLUMN RANGE
───────────────────────────────────────────────────────────────────────────
    4           │  0-319            │  0-199
    5           │  0-319            │  0-199
    6           │  0-639            │  0-199
    13           │  0-319            │  0-199
    14           │  0-639            │  0-199
    15           │  0-639            │  0-349
    16           │  0-639            │  0-349
───────────────────────────────────────────────────────────────────────────

        The function writec() writes a character or a string of identical
characters, starting at the current cursor position. It does not change the
cursor position. The number of repetitions must not cause the function to
write past the end of the current line.
        This function is particularly handy for quickly drawing lines and
boxes on the screen. We will also use it as a building block in functions
that manipulate higher-level objects, such as text strings that are
presented in fixed-length fields and scrolling windows.

    /*      writec -- write a character and attribute to the screen   */

    #include <dos.h>
    #include <local\std.h>
    #include <local\bioslib.h>

    int writec(ch, count, pg)
    unsigned char ch;       /* character */
    unsigned char attr;     /* attribute */
    int count;              /* number of repetitions */
    int pg;                 /* screen page for writes */
    {
            union REGS inregs, outregs;

            inregs.h.ah = WRITE_CHAR_ATTR;
            inregs.h.al = ch;
            inregs.h.bh = pg;
            inregs.h.bl = attr;
            inregs.x.cx = count;
            int86(VIDEO_IO, &inregs, &outregs);

            return (outregs.x.cflag);
    }

        The writeca() function is the same as writec(), except that it also
writes the video attribute at the same location or region.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 100 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        To simulate the printing behavior of a simple terminal, use the
writetty() function to place characters on the screen. This function writes
characters only, but it automatically does newline and screen-scrolling
operations.

    /*      writetty -- write to screen using TTY interface   */

    #include <dos.h>
    #include <local\std.h>
    #include <local\bioslib.h>

    int writetty(ch, attr, pg)
    unsigned char ch;       /* character */
    unsigned char attr;     /* video attribute */
    int pg;                 /* screen page for writes */
    {
            union REGS inregs, outregs;

            inregs.h.ah = WRITE_TTY;
            inregs.h.al = ch;
            inregs.h.bl = attr;
            inregs.h.bh = pg;
            int86(VIDEO_IO, &inregs, &outregs);

            return (outregs.x.cflag);
    }

        The writedot() function writes a dot of the specified color at the row
and column position passed as arguments. Valid color numbers are a function
of the current graphics mode:

═══════════════════════════════════════════════════════════════════════════
    MODE                        COLOR NUMBERS
───────────────────────────────────────────────────────────────────────────
    4, 5, 10                │   0-3
    6                       │   0 and 1
    8, 9, 13, 14            │   0-15
    15                      │   0-1
    16                      │   0-4 or 0-15
───────────────────────────────────────────────────────────────────────────

If bit 7 of the AL register is a 1, the dot color is exclusive-ORed (XOR)
with the current dot color value.

    /*      writedot -- display a dot at the specified position   */

    #include <dos.h>
    #include <local\std.h>
    #include <local\bioslib.h>

    int writedot(r, c, color)
    int r, c;       /* row and column coordinate */
    int color;      /* dot (pixel) color */
    {
            union REGS inregs, outregs;

            inregs.h.ah = WRITE_DOT;
            inregs.h.al = color;
            inregs.x.cx = c;
            inregs.x.dx = r;
            int86(VIDEO_IO, &inregs, &outregs);

            return (outregs.x.cflag);
    }

        The following miscellaneous video functions are used to clear the
current screen page, or a portion of it, scroll a region of the screen up
or down, set the graphics palette or text border color, and write video
attributes.
        The clrscrn() function uses the BIOS video scroll routine to
initialize the visual screen page. The clrw() function clears a window of
specified dimensions on the visual screen page.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 102 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 103 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        The palette() function serves two purposes. In text modes, it sets the
screen border color. In graphics modes, it sets the color palette from
which drawing colors are selected.

    /*      palette -- set graphics color values or border color   */

    #include <dos.h>
    #include <local\bioslib.h>

    int palette(id, color)
    unsigned int id, color;
    {
            union REGS inregs, outregs;

            inregs.h.ah = PALETTE;
            inregs.h.bh = id;
            inregs.h.bl = color;
            int86(VIDEO_IO, &inregs, &outregs);

            return(outregs.x.cflag);
    }

        To scroll the entire visual screen page or a rectangular portion of it
up or down, use scroll(). The function takes a signed number: negative
numbers scroll lines down the screen, nonzero positive numbers scroll lines
up, and zero initializes the specified region. Vacated lines are set to
blanks in the specified attribute.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 104 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        The next function is really a composite of two BIOS video services. To
change the attribute of a range of character positions without disturbing
the characters, use writea(). This function reads the character and
attribute at each affected position and writes back the same character in
the new attribute while leaving the current cursor position unchanged.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 105 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


Demonstration Program

        Now that we have a collection of DOS and BIOS interface routines, what
can we do with them? Perhaps the best way to answer the question is to
demonstrate their use in a program. A vehicle for showing off some of the
routines is CURSOR, a program that magnifies the current cursor and lets us
change its shape. CURSOR is proof that carefully written and applied BIOS
video routines can produce startlingly fast screen manipulation--far from
instantaneous, but fast enough for demanding commercial applications.
        Before examining the source code for CURSOR, we will prepare several
functions that are built on the current set of BIOS video routines. The
functions put_ch(), putstr(), and drawbox() can be viewed as a layer above
the writec() and putcur() functions of the BIOS library just described.
They provide often-used capabilities that are not provided in a convenient
way by the primary BIOS functions.
        The put_ch() function displays a single character at the current
cursor position and then advances the cursor to the next position. The name
includes the underscore to distinguish put_ch() from putch(), a standard
console I/O routine in the Microsoft run-time library. The putstr()
function displays a text string and advances the cursor to the next
displayable position. Combining these two functions and several of the low-
level BIOS functions in drawbox() lets us create fine-ruled boxes for
highlighted text and graphical elements of our screens. The drawbox()
function uses the single-line drawing characters that are part of the IBM
extended-ASCII character set. We could also use double-line and other
combinations of line-drawing characters to produce distinctive box shapes,
as we will do in later chapters.
        Here are the C source listings for put_ch(), putstr(), and
drawbox():

    /*      put_ch -- display a character in the prevailing video
    *      attribute and advance the cursor position   */

    #include <local\video.h>

    int put_ch(ch, pg)
    register char ch;
    int pg;
    {
            int r, c, c0;

            readcur(&r, &c, pg);
            writec(ch, 1, pg);
            putcur(r, ++c, pg);
            return (1);
    }



    /*      putstr -- display a character string in the
    *      prevailing video attribute and return number
    *      characters displayed   */

    int putstr(s, pg)
    register char *s;
    unsigned int pg;
    {
            unsigned int r, c, c0;

            readcur(&r, &c, pg);
            for (c0 = c; *s != '\0'; ++s, ++c) {
                    putcur(r, c, pg);
                    writec(*s, 1, pg);
            }
            putcur(r, c, pg);
            return (c - c0);
    }


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 108 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        Now we can write cursor.c. The goal is to design a program that
presents a magnified view of the cursor and that allows us to interactively
adjust the cursor shape. The source for CURSOR is quite long. It is one of
several programs I wrote to test the DOS and BIOS interface functions. The
program shows how the DOS and BIOS interface functions are used in a
working example. By writing "driver" programs as I wrote the interface
functions, I was able to both anticipate what functions might be needed and
test the functions as they were written.
        The pseudocode for CURSOR shows how the program works.

    get video information
    save current attribute/color values
    draw basic screen (header, cursor image, instructions)
    while (key is not Return)
            switch selection modes on left or right arrow
                clear start/stop selection pointer
                display new pointer
            adjust scan line position on up or down arrow
                clear current scan-line pointer
                calculate new pointer value
                update cursor image display
                    (scan lines, pointers)
    set new cursor type
    restore original attribute/color
    clear screen

        This simple description does not reveal a few details in the
calculations needed to accurately display images for cursors on various
types of hardware. For example, CURSOR automatically adjusts the displayed
image to compensate for 80- and 40-column screen widths, and differing
numbers of scan lines and widths in cursors on IBM monochrome display
systems (9 by 14) and color/graphics systems (8 by 8). The C source, which
shows how these details are handled, is in the file cursor.c.



                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 110 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        The makefile, bios.mk, automatically builds the library bios.lib using
the inference rules supplied by tools.ini. The makefile for CURSOR is
contained in cursor.mk:


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 116 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


# makefile for CURSOR program

LLIB=c:\lib\local

cursor.obj:    cursor.c
        msc $*;

cursor.exe:    cursor.obj $(LLIB)\bios.lib  $(LLIB)\dos.lib
        link $*, $*, nul, $(LLIB)\bios $(LLIB)\dos;

        When you run the CURSOR program, some computers will exhibit
unexpected behavior as a function of the installed hardware. The AT&T
PC6300, for example, does not permit "split" cursors (the stop scan line
is less than the start scan line). CURSOR will allow you to fashion a split
cursor, but on the 6300, it disables the cursor instead. Also, I have not
modified the program to work with EGA-compatible boards. EGA boards use
different numbers of scan lines per character cell than standard CGA boards
operating in the same (or equivalent) modes.

        Before moving on to user interfaces, we will add one more function to
the BIOS video library. The writestr() function is not used in this
chapter, but it is called by a user-input function in Chapter 6. It
displays a two-part message in a fixed field and leaves the cursor at its
current position. The design of writestr() lets us concatenate strings
visually and prevents them from extending beyond the available display
area. In addition, if the resulting string does not completely fill the
display field, writestr() pads the field with spaces in the prevailing
attribute to clear any residue from the previous contents of the field.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 118 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


Recommendations

        We need to address a few library management issues. In my work and in
this book, I have elected to keep the number of functions at one per file
except in rare circumstances where two or more functions have a symbiotic
relationship. The cost of doing this is increased disk use. Although most
of the source files are less than 500 bytes in size, they each require from
2 to 8 KB of storage capacity on a typical hard disk. The amount used is a
function of the block or cluster size of the disk, which is in turn a
function of its capacity and the version of DOS used to format the
disk.
        The primary advantage of separate functions in libraries is that the
linker program adds the object code only for the functions used in a
program, thus avoiding the possibility of dragging in a lot of unneeded
instructions.
        The alternative--packaging related functions in a single object module
in a library--can be effective too, as long as the functions so grouped are
likely to be used together in a program. We could, for example, group the
getctype(), setctype(), and other cursor-related functions in a single
object module because they are usually employed together, as in the CURSOR
example.
        The issue of compatibility across a wide range of machines deserves
comment. To obtain the broadest PC compatibility for your programs, use the
higher-level DOS function calls. They are often slower and less versatile
than the BIOS functions, but programs built on them can be run on virtually
any machine that runs DOS.
        Using BIOS interfaces will erode your market a little, but probably
not enough to fret about for more than a few seconds. If a large enough
market exists for a non-compatible machine (the Zenith Z-100, for example),
you can create a customized version of the program because the dependencies
can be limited to a few interface library functions.
        Compatibility of PC/DOS-based programs to UNIX and XENIX is another
matter. Some fundamental differences in architecture make transporting a
program developed under DOS on a PC to a UNIX-based system difficult, but
not impossible. The primary difficulty is that users in a multi-user
environment are typically not using the console; they are connected by
dial-up or hard-wired lines to the host computer. Those connections are a
bottleneck that reduces the available "bandwidth" of the user interface to
a tiny fraction of what a typical PC has available on a dedicated machine.
Despite this difficulty, most programs can be written for portability
between DOS and UNIX/XENIX. In fact, I have done most of my development
work under XENIX and then converted to DOS afterward.
        Going from DOS to UNIX is tougher because I tend to take advantage of
all that the PC and DOS offer for program performance reasons. To gain some
needed portability to UNIX, we can use the Termcap/Curses virtual terminal
interface packages that permit UNIX to work with just about any
alphanumeric video terminal. Termcap is a set of low-level functions that
get terminal information from a database of terminal capabilities and
provide control over cursor positioning and other video terminal
functions.
        Curses is a high-level screen-management package that provides
optimization of cursor movements and control over displayed text, video
attributes, and keyboard interactions. Curses also supports buffers that
are larger than the screen and provides modest windowing features.
        It is outside the scope of this book to provide detailed information
on Termcap/Curses software. You may want use the virtual-terminal interface
in your programs for one important reason: A program written under DOS on a
PC using a Curses interface for screen management is easily ported to
UNIX/XENIX. Give it a shot. A very good Curses implementation for the PC
from Aspen Scientific works well with Microsoft C and other C compilers
under DOS and is compatible with UNIX System V Curses. Lattice offers a
non-optimizing version of Berkeley Curses as an adjunct to its C compiler.
Other Curses implementations are available, but I have not tested
them.
        There is much more to the DOS/BIOS interface than we have covered in
this chapter. As we need additional functions, we can use what we have done
so far as models for the needed functions. Next, we look at one of the most
important aspects of any program--the user interface.



Chapter 6  The User Interface



        The user interface has received a lot of attention in recent years.
Human factors engineering (ergonomics)--the endeavor that is one part each
of engineering, science, and "black magic"--is best known for dealing with
such topics as how to design safe, comfortable car seats and how to lay out
the cockpit of a jet aircraft. However, it also offers much information
about how people use machines that have neither wheels nor wings. Although
by no means a dissertation on human factors engineering, this chapter
focuses on the primary points of contact between the computer and the
user--the keyboard, the display screen, and the speaker--and on how to use
these communications pathways effectively.
        As programmers, we must consider both the application and the audience
for the application. In addition, we need to know about the environment in
which a program is to be used, in order to provide the best possible match
between the program and its intended users. Some programs lend themselves
to a command-oriented type of operation. Frequently used tools that may
become components of pipeline commands are examples of this category. Other
programs are well suited to windowing and menus, or at least benefit from
their use. Programs that are infrequently used and that may involve complex
step-by-step procedures fall into this category. Still other programs can
successfully blend the two approaches or switch easily between them. In
Chapter 13, we will develop a program that provides convenient screen
control with instructions from either the DOS command line or a simple menu
of commands. The mode of operation depends on how the program is
called.
        The use of pleasing visual effects and appropriate sound can do a lot
to enhance both the appeal and the utility of a program. However,
effectively using visual effects and sound in programs is no less an art
than playing a musical instrument. Later in this chapter, we will explore
the use of sound and text-oriented visual effects in our programs. But we
will start with a view toward standardizing the way programs gather and
process command-line options. Then we will develop some timer functions
that are needed to do several important jobs.


Standard Command-line Processing

        The standard user interface of unadorned UNIX/XENIX and DOS is the
command line. One of the major modes of communication from the user to a
program is via command-line options and other arguments. For years, there
has been a need to standardize command-line option processing by programs
in order to provide a clean and consistent user interface. This was evident
long before CP/M and DOS were conceived in the fertile minds of their
developers; the problem exists in Multics, UNIX, XENIX, and many other
minicomputer and mainframe operating systems. Here is an example of the
problem.
        Under UNIX, command-line options are introduced by a leading dash (-).
An option letter selects a particular program option and may require an
option argument. Therefore, -w80 might be used to select a page width (w)
of 80 columns when used with a printer program. The option argument--80, in
this case, but other values may be allowed--must be entered if the w option
letter is used.
        An early version of UNIX (Version 7 in commercial circles) requires
that options to the print command, r, be invoked individually, each having
its own option flag and letter. Thus, printing a file in multiple columns
without the usual header and footer lines requires a command constructed
according to the following template:

        pr -2 -t filename

in which the -2 and -t arguments specify the desired options, and filename
is the lone filename argument.
        Later versions of UNIX, starting with the release of System III,
permit the option letters to be combined and introduced by a single option
flag, so a command of the form

        pr -2t filename

does precisely the same job as the first one shown. A command typed
according to the older specification (separate option flags) is also
allowed.
        To complicate matters, some programs enforce the requirement that
option letters that take arguments must have a space or tab between the
letter and the argument while other programs require no white space (-w 80
vs -w80, for example). These variations are the result of no small amount
of anarchy among the developers and are the source of the confusion from
which standards eventually evolve.
        So why should this matter to DOS programmers and users? Well, for one
thing, DOS resembles UNIX in many ways and emulates many UNIX features and
principles. For another, the same kinds of problems already exist under
DOS. Some commands take option switches, usually indicated by the forward
slash (/), but some programs accept the dash (-) as well. The option
switches usually are placed after the command name but before any
filenames, yet some commands require that option switches follow everything
else on the command line. Still other programs don't care where the option
flags and arguments go--they simply scan the entire command line before
doing anything.
        Under recent versions of UNIX and XENIX, a utility function named
getopt() is used to process option flags and arguments in a consistent way.
The getopt() function can be used by any DOS program that accepts command-
line options and arguments. We will apply it to a sample utility program,
CAT, which concatenates files (one or more) onto the standard output
stream.
        Figure 6-1 on the next page shows us how getopt() takes command
lines and sensibly parses options and arguments. We have already seen
in Chapter 3 how information in the DOS command tail is made available
to a program via the argc and argv parameters to the main() function and
how a program can be written to use the information in the command tail. We
have not yet, however, agreed on a format for commands.
        For the programs in this book, we will present command lines according
to a common template. The command name always appears first (this is an
operating system requirement), followed by options, if any, followed by
other data, typically filenames, if any. Options are introduced by a
leading dash. Such a template works well for most of the commands we will
encounter. However, with the more complicated commands, we probably will
provide a menu-style interface instead of a command-oriented interface.


    C> cat -s *.c linebuf .h
            ░░░░░░░░░░░░░░░░░► the command tail
                                is processed by setargv()
══════════════════════════════════════════════════════════════════════════
    argc
    ┌────┐
    │▒6▒▒│           ┌────┬────┬────┬────┐
    └────┘  ┌───────►│ C  │ A  │ T  │ \O │   (DOS version 3.0 and later)
            │        └────┴────┴────┴────┘
            │
            │             ┌────┬────┬────┐
    ┌────┐  │  ┌─────────►│ -  │ s  │ \O │
0 │▒▒■─┼──┘  │          └────┴────┴────┘
    ├────┤     │
1 │▒▒■─┼─────┘                    ┌────┬────┬────┬────┬────┬────┬────┬────┐
    ├────┤        ┌────────────────►│ F  │ I  │ L  │ E  │ 1  │ .  │ C  │ \O │
2 │▒▒■─┼────────┘                 └────┴────┴────┴────┴────┴────┴────┴────┘
    ├────┤
3 │▒▒■─┼────────┐                 ┌────┬────┬────┬────┬────┬────┬────┬────┐
    ├────┤        └────────────────►│ F  │ I  │ L  │ E  │ 2  │ .  │ C  │ \O │
4 │▒▒■─┼─────┐                    └────┴────┴────┴────┴────┴────┴────┴────┘
    ├────┤     │
5 │NULL│     │          ┌────┬────┬────┬────┬────┬────┬────┬────┬────┬────┐
    └────┘     └─────────►│ l  │ i  │ n  │ e  │ b  │ u  │ f  │ .  │ h  │ \O │
                        └────┴────┴────┴────┴────┴────┴────┴────┴────┴────┘

        FIGURE 6-1 ▐ Expanding ambiguous filename specifications


        Ambiguous filename arguments are expanded automatically by the setargv
routine if it is included in the link list. The argument vector array,
argv, contains pointers to strings in memory. The DOS command tail is
everything that follows the command name on the DOS command line, including
the initial space or tab that separates the first option or argument from
the command name. Although the DOS command tail is limited to 128 bytes,
ambiguous file names can be expanded to produce effective command tails of
much greater length. The figure illustrates what the pointers and strings
would look like for the command line

        cat -s *.c linebuf.h

if the current directory contains the two C source files, file1.c and
file2.c, the header file linebuf.h, and possibly other files. Consider what
this command might produce if invoked in a directory with many C source
files.
        The two C source-file names are stored with all letters converted to
uppercase because DOS always converts to uppercase when it expands names.
The -s option and the header file name are stored as literal copies because
no expansions are required. The command name, available only under DOS 3.00
and later versions, is presented with all letters converted to uppercase
regardless of how they are typed by the user, again because of a DOS
convention.
        The following source for getopt() was provided by AT&T. It is the
current version of getopt() that is available from the AT&T UNIX System
Toolchest.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 127 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        The following synopsis and description of getopt() are based on its
observed behavior under UNIX System V, Release 2, the current UNIX
standard. If we use this standard, programs developed under DOS that do not
employ any hardware-dependent code will be readily transportable to
UNIX/XENIX (and vice versa).
        The getopt() function receives an argument count and an array of
pointers to argument strings (usually copies of the argc and argv
parameters of main()) and a string variable that is a list of allowable
option flags. Single letters (uppercase and lowercase letters are unique)
and  single-digit numbers are acceptable option letters. If a valid option
letter in the list is followed by a colon, getopt() expects to find a
following option argument.
        In addition to the parameters passed to the function, the interface to
getopt() involves three global variables. The optind variable, the option
index, is an integer that is initialized to 1; it keeps track of which
option is currently being processed. A character pointer, optarg, is set to
NULL unless the option being processed takes an argument, in which case
optarg points to what should be the required argument. The third global
variable is opterr. It is initialized to 1, which causes getopt() to report
errors such as an option flag that is not a member of the passed list or a
missing argument. Our programs can shut off error messages from getopt() by
setting opterr to 0. Although optind and optarg must be declared as
external variables in our programs, we need to declare opterr only if we
intend to change its value.

                                COMMENT
            The UNIX and XENIX manual pages for getopt() contain an
            error. The value of optind does not default to 0. It is
            initialized to 1. Also, previous versions of getopt()
            emitted error messages when opterr was 0. The latest
            versions of getopt() have reversed the sense of the
            opterr variable and emit error messages if opterr is 1
            (the default value).

        The pseudocode for getopt() reveals the complexities of handling user
input in a general way. The primary difficulty is in parsing optional
arguments that may or may not be separated from the associated option
letter by white space.

            index to first argument following command name
                        (done before the first call to getopt())
            if (no arguments OR
            arg not an option OR
            flag without option letter)
                    return EOF
            else if (special end-of-options indicator)
                    skip over argument
                    return EOF
            if (option letter not in option string)
                    if (opterr is non-zero)
                            print error message
                    return '?'
            if (option letter followed by ':' in option string)
                    if (option argument found)
                            set optarg to start of argument
                            return option letter
                    else
                            if (opterr is non-zero)
                                    print error message
                            return '?'
            else
                    set optarg to NULL
                    return option letter

        To show how the getopt() function is used in practice, we will now
write a program that uses getopt() in its simplest form. Later in this
chapter and in other chapters, we will use getopt() in more complex
argumentprocessing situations.
        The CAT program, presented next, uses getopt() to process a single
option. CAT is used to concatenate files. It accepts a list of zero or more
files and produces a continuous data stream on its output. If only a single
file is named, CAT simply lists the file's contents on the screen. Figure
6-2 on the next page is the manual page for CAT.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the figure found on page  ║
                ║ 132 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝

        FIGURE 6-2 ▐  Manual page for CAT


        The source for CAT follows:


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 132 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        CAT duplicates its input stream on its output stream by calling
fcopy(). The input comes from files named on the command line or from stdin
if no files are named. Output is to the standard output stream, stdout.
Therefore, CAT is minimally a filter. The transformation it performs is to
merge data streams into a single sequential output stream. CAT is most
frequently used to list the contents of one file on the console screen or
to combine a group of files into a single file. It may be used as the
source node of a pipeline command, either taking its input from named files
or the console.
        Following is the C source for fcopy().

    /*
    *      fcopy -- copy input stream (fin) to output stream (fout)
    *      and return an indication of success or failure
    */

    #include <stdio.h>

    int fcopy(fin, fout)
    FILE *fin, *fout;
    {
            int errcount = 0;
            char line[BUFSIZ];
            char *s;

            while ((s = fgets(line, BUFSIZ, fin)) != NULL)
                    if (fputs(s, fout) == EOF)
                            ++errcount;
            if (ferror(fin))
                    ++errcount;
            return (errcount);      /* 0 if all went well */
    }

Since both getopt() and fcopy() are going to be useful in other programs,
add them to the utility library.
        Now that we have a function that lets us process command lines in a
consistent way, we can move on to other aspects of the user-machine
interface. We will now look at methods of producing machine-independent
time measurements and delays.


Timing Functions
        The routines and programs in this and the next subsections show how to
time events, produce time delays, and create sounds. All depend in some way
upon the PC's built-in timer circuits. We begin with a program called
TIMER, which uses some of the ctime subroutines in the standard library
that we examined in Chapter 4.
        TIMER is an external program that lets us time intervals from the DOS
command level and from within batch files. The program is handy for timing
events lasting from a few seconds to a few days. It is not suitable for
shorter intervals because of the way it is implemented. A typical use
consists of starting a timer, running a program or performing a task, and
then calling the timer again to check the elapsed time. Figure 6-3 is the
manual page for TIMER, and its source code is in timer.c.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the figure found on page  ║
                ║ 136 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝

        FIGURE 6-3 ▐  Manual page for TIMER


        The source for TIMER follows:


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 137 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        The source for TIMER illustrates a slightly more complicated use of
getopt() than we saw in the CAT program. In addition to handling more
option flags, TIMER has one option flag that requires an argument. The
option -f takes an argument that specifies a filename; if -f is present,
the next argument is presumed to be the required argument. The user can
insert extra white space characters between the option flag and the
argument, but it's not required. When getopt() detects the -f option, it
immediately sets optarg to point to the start of the argument string. The
program calling getopt() then interprets the argument.
        The TIMER program uses a special area in memory that is called the
intra-application communications area (ICA). The ICA resides in an area of
memory (addresses 4F0--4FF hex) that is reserved by DOS. The ICA is a mere
16 bytes in length, but it's big enough to store four long integers, one
for each of four separate timers. Few commercial programs use the ICA, but
TIMER should protect itself against corruption of its data. Therefore, each
timer value is represented by the lower 30 bits (0--29) of a four-byte
long. The top two bits are used for status and identification purposes. The
ID bit, bit 30, must be a logical value of 1. The most significant bit, bit
31, is also set to a logical value of 1 to indicate that a timer value has
been stored. If these bits do not have the correct values, attempts to show
an elapsed time produce an error message.
        The time() library function returns a long integer that is the number
of seconds since the epoch (00:00:00 on January 1, 1970 for ctime
subroutines). There are 31,536,000 seconds in a normal year (add one day,
86,400 seconds, for a leap year). Using the first 30 bits of a long to hold
the time value gives us a range of 34 years, so TIMER will function
correctly until 2004--by which time I hope to have a new computer and
operating system. We can extend the span to 68 years by not using a
separate ID bit, but that increases our risk of returning incorrect elapsed
time values if some other program happens to use the ICA.
        TIMER is a small-model program because it requires little code space
and even less data space. However, the ICA is in the BIOS segment, not the
program's data space. TIMER uses the movedata() library function to read
and write data in the ICA. Because movedata() requires two segmented
addresses, TIMER also calls the segread() library function to get the data
segment register value. The other address needed by movedata() is in the
BIOS data segment. I chose to use 0x4F as the segment value (ICA_SEG) and
an offset of 0 for the ICA, but these can be reversed if you prefer.
        When the -e flag is given, TIMER calls interval() with the number of
seconds between "now" and when the timer was started. The saved timer is
not altered. The interval() function translates the elapsed seconds into
ASCII text using the form hh:mm:ss. If no timer number is specified, timer
0 is used. The user may restart an interval timer by using the -s (start)
option along with the timer number.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 141 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        I have found TIMER to be most useful for recording work time against
project accounts in my consulting practice and also as a means of measuring
program execution times. Using the -f option, all data that would normally
appear on the console screen is instead appended to the named file, which
becomes a handy record of activities. When testing program execution
speeds, I minimize the effects of disk loading times by setting up a
virtual disk for the batch file, the TIMER program, and the data file.
Everything else is run from a hard disk or a floppy disk so that loading
and access times for the program will be taken into account. This beats
using a stopwatch and ink on paper.


        The PC Timer and Time Delays

        Following is a brief description of the timer circuits in the PC and a
look at some of the internal programs that keep track of time in various
ways. Refer to Figure 6-4 while reading through this description. The
figure shows the basic elements of the PC's timer and speaker-control
circuits. We can ignore the speaker elements (shaded boxes) for now
and concentrate our attention on the clock signal generator and
timer/counters 0 and 1.


                ╔═════════════════╗
                ║  ┌───────────┐  ║
┌───────────┐┌─╫─►│clk        │0 ║
│1.19318 MHz││ ║  │        out├──╫─────────► timer interrupt
│from system││┌╫─►│gate       │  ║
│clock      │││║  └───────────┘  ║ ┌───────► RAM refresh
└─────┬─────┘││║                 ║ │
        ▼      ││║  ┌───────────┐  ║ │
        •──────•┼╫─►┤clk        │1 ║ │  AND gate              Low-pass     /│
        │ tied  │║  │        out├──╫─┘   ┌──       driver     filter      /▒│
        │ high ─•╫─►│gate       │  ║┌───►│▒▒▒\     ┌──────┐  ┌──────┐  ┌─┐▒▒│
        │        ║  └───────────┘  ║│    │▒▒▒▒)───►│▒▒▒▒▒▒│─►│▒▒▒▒▒▒│─►│▒│▒▒│
        │        ║                 ║│ ┌─►│▒▒▒/     │▒▒▒▒▒▒│  │▒▒▒▒▒▒│  └─┘▒▒│
        │        ║  ┌───────────┐  ║│ │  └─        └──────┘  └──────┘     \▒│
        └────────╫─►│clk ▒▒▒▒▒▒▒│2 ║│ └──────────────────────┐             \│
                ║  │▒▒▒▒▒▒▒▒out├──╫┘   ╔════════════════════╪══════╗
                ┌─╫─►│gate▒▒▒▒▒▒▒│  ║    ║  port 61 hex       │      ║ speaker
                │ ║  └───────────┘  ║    ║ ┌─────────────────┬┴─┬──┐ ║
                │ ╚═════════════════╝    ║ │▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒│1 │0 │ ║
                │ 8253-5 timer/counter   ║ └─────────────────┴──┴┬─┘ ║
                │                        ╚═══════════════════════╪═══╝
                └────────────────────────────────────────────────┘
                                        8255 programmable
                                        peripheral interface (PPI)

        FIGURE 6-4 ▐  Timer and speaker control circuits


        The primary clock rates for the system unit and peripheral interfaces
are derived from a high speed crystal-controlled oscillator, an 8284A
device. One of the output signals is divided down to 1.19318 megahertz
(MHz) and is fed to the 8253-5 timer/counter on the CLK input of all three
channels. (An 8254-2 timer/counter circuit is used in the PC/AT.) Channel 1
is used to refresh main memory; we should not alter this channel in any
way. Channel 2 will be described when we cover sound generation. Of primary
interest to us now is channel 0, which is used to provide a system-wide
timer interrupt (interrupt 8). This interrupt is called a clock tick.
        A clock tick occurs at a rate of about 18.2 per second, or one
approximately every 55 milliseconds. The number of ticks per second is the
timer input clock rate, 1.19381 MHz, divided by 65536, which is the
divisor on timer channel 0. Other divisors may be used to produce different
interrupt frequencies.
        Each timer/counter channel contains a 16-bit counter, a pair of 8-bit
latches to hold the starting count, a pair of 8-bit output latches, and
control logic. Each input clock pulse decrements the value held in the 16-
bit counter until the value is 0. Setting a count of FFFF hex gives us a
divisor of 65535, which is one less than needed. If, however, we set the
starting count to 0, the first input clock pulse will cause the counter to
go from 0 to 65535 (0-1=-1; all bits set to logical 1). This technique
produces an effective divisor of 65536, the correct value, because the
counter is treated as an unsigned integer. Consult the Intel timer/counter
documentation for more information on how it works and how it may be
programmed.
        The ticks are used by the ROM BIOS to update the time-of-day (TOD)
clock, which is stored as the number of ticks since midnight. Therefore,
any program that modifies the count on timer channel 0 must compensate for
the change to maintain the tick rate seen by BIOS. We can use the same 18.2
ticks per second as the basis of some machine-independent timing functions
to produce delays and to create sound.
        We may want to build delays into our programs for a variety of
reasons. A program that has a banner frame (I like to call it the "ego
frame") must be seen to be appreciated, so it is appropriate to hold it on
screen for a few seconds. Automated "slide shows" need controllable delays
between frames to pace the presentation. Accurate short-duration delays are
also needed to produce audible sound effects.
        When there was only a single PC model, many programmers were not too
careful about how time delays were produced. We often depended upon the
characteristics of the machine by using loops that went through the right
number of iterations to waste the required time. Of course, faster
processors and higher clock rates in many of the newer computers have
shrunk delays produced this way to as little as an eighth of their former
selves. We are now in need of a way to produce reliable, machine
independent time delays.
        The delay() function uses the computer's timer to generate delays that
are consistent with the entire family of IBM PCs and portable to most
compatible machines. In order to produce the required waiting period, the
delay() function converts the specified period into a number of timer
ticks. It does so by multiplying the period by TICKRATE, which is defined
in timer.h in the \include\local directory. It then adds that amount to the
current number of clock ticks obtained from the time-of-day counter and
stores the sum. Then getticks() is called repeatedly by delay(). The
delay() function exits when the value returned by getticks() equals or
exceeds the target value.
        The getticks() function queries the TOD clock and returns the current
time of day as a count of clock ticks. One modification is made when the
TOD clock rolls over from one day to the next: Since the TOD clock count is
reset to zero and a flag is set to indicate the day increment, the
getticks() function adds a day's worth of ticks (1,573,040 [1800B0 hex]) to
the count it returns if the flag is nonzero. Since all machines in the IBM
product line (and most compatibles) use the 1.19381 MHz timer clock rate,
the delay period is the same on all of the machines and is totally
independent of the CPU speed and master clock rate of the machine.
        The following files contain the sources for the header file, timer.h,
and the functions delay() and getticks():

    /*
    *      timer.h -- header for timer control routines
    */

    /* timer clock and interrupt rates */
    #define TIMER_CLK       1193180L
    #define TIMER_MAX       65536L
    #define TICKRATE        TIMER_CLK / TIMER_MAX

    /* timer port access for frequency setting */
    #define TIMER_CTRL      0x43
    #define TIMER_COUNT     0x42
    #define TIMER_PREP      0xB6


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 145 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 146 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        The delay() function has general applicability and will be used in
other programs. We need to put it someplace that is easy to access. Because
delay() is based on the getticks() function, which in turn uses the ROM
BIOS, we will add both of these functions to the bios library. And, of
course, timer.h takes a permanent home in \include\local.


        Audible Feedback

        We now turn our attention to sound generation. The first thing to
notice about sound is that it irritates some users. Also, sound is totally
inappropriate in some settings. Therefore, we must provide a way for the
user to disable sound. The programs in this book will use a variable,
usually the global variable Silent, to record the user's preference
regarding sound. If Silent is logically TRUE, our program will be mute.
        The shaded boxes in Figure 6-4 on page 143 show the PC
sound system. Notice how the sound system is built upon channel 2 of the
timer subsystem and a couple of I/O ports in the 8255-5 programmable
peripheral interface (PPI) device. The timer/counter produces squarewave
signals that are amplified and filtered, then passed to the speaker. The
AND gate ahead of the driver is controlled by bit 1 of the PPI port 61 hex.
In addition, bit 0 of the same port controls the gate lead of timer channel
2. Thus, the PPI can be used to turn sound on (both bits a logical 1) or
off (either bit a logical 0). The advantage of using the timer to produce
sound is that the sound plays in the "background," and does not steal
precious cycles from the CPU.
        Alternatively, we can disable the timer by setting port 61 hex, bit 0
low, and pulse the speaker directly under program control via bit 1. The
problem with this approach is that the program can do nothing else while it
attends to the speaker. We will not use this approach.
        The file sound.h, also in \include\local, is the header file for our
sound routines. It contains definitions and macros used to control the
speaker's state. SPKR_ON sets the low two bits in the peripheral interface
port to logical 1s. This enables the timer and passes signals through to
the speaker. The SPKR_OFF macro sets both bits low, turning the speaker off
by robbing it of input signals.

    /*
    *      sound.h -- header for sound routines
    */

    #define PPI       0x61
    #define SPKR      0x03
    #define SPKR_ON   outp(PPI, inp(PPI) | SPKR)
    #define SPKR_OFF  outp(PPI, inp(PPI) & ~SPKR)

        As noted already, the speaker can operate independently, letting us
put sounds in the background. We can demonstrate this with a simple control
program, SPKR. This program turns the speaker off if it is called with no
arguments and on if it receives any other number of arguments. The source
for this test driver is contained in spkr.c (on the following page).

    /*
    *      spkr -- turn speaker ON/OFF
    *
    *              no args => OFF
    *              any arg(s) => ON
    */

    #include <local\sound.h>

    main(argc, argv)
    int argc;
    char **argv;
    {
            /* turn speaker on or off */
            if (argc == 1)
                    SPKR_OFF;
            else
                    SPKR_ON;
            exit(0);
    }

        The program has one problem--it does not set the pitch of the tone the
speaker emits. This must be done by another program, TONE, a simple test
driver that lets us set the pitch from the DOS command line. TONE takes a
single argument that specifies the desired frequency in hertz. The usable
range for the PC's internal speaker is about 40 Hz to a little more than 6
KHz. Higher frequencies can also be used; however, these may cause some
speakers to produce barely audible tones or clicking sounds. TONE calls
upon a low-level routine, setfreq(), to calculate the needed divisor for
the specified frequency and to set up the timer's channel 2, from which the
speaker derives its input. The setfreq() function includes the timer.h
header file which contains the declarations for the port addresses and
values needed to set the frequency of the timer.
        The code for TONE and setfreq() follow:

    /*
    *      tone -- set the frequency of the sound generator
    */

    #include <stdio.h>

    main(argc, argv)
    int argc;
    char **argv;
    {
            extern void setfreq(unsigned int);

            if (argc != 2) {
                    fprintf(stderr, "Usage: tone hertz\n");
                    exit(1);
            }

            /* set the frequency in hertz */
            setfreq(atoi(*++argv));
            exit(0);
    }



    /*
    *      setfreq -- sets PC's tone generator to run
    *      continuously at the specified frequency
    */

    #include <conio.h>
    #include <local\timer.h>

    void
    setfreq(f)
    unsigned f;     /* frequency in hertz (approximate) */
    {
            unsigned divisor = TIMER_CLK / f;

            outp(TIMER_CTRL, TIMER_PREP);           /* prepare timer */
            outp(TIMER_COUNT, (divisor & 0xFF));    /* low byte of divisor */
            outp(TIMER_COUNT, (divisor >> 8));      /* high byte of divisor */
    }

        To use these programs to demonstrate some of the capabilities of the
sound system, compile and link them, and then type

        TONE 1000
        SPKR ON

This sets an audible pitch and turns on the speaker. You can do other work
and the tone will continue to play unless some other program turns off the
speaker. Typing

        SPKR

without any arguments turns off the speaker.
        You can simulate a burglar alarm sound if you have a perverse
nature. Key in the code for sweep.c, compile it and link it with the
setfreq.obj file, and type

        SWEEP

To stop the sound before the police arrive, press any key.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 150 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        All this noise making is wonderful for something, I'm sure, but we
will want finer control of the sound subsystem in our programs. We obtain
that control via the sound() function, our high-level sound interface
function. Using sound(), we can produce distinctive sounds such as a
confirmation signal (confirm()), a warning signal (warn()), and many
others. The source for the sound() function is contained in sound.c.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 151 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        Notice that the speaker is turned on, continues to sound during the
specified interval, and is then turned off for each tone being generated.
Earlier demonstration programs turned on the speaker once, issued a series
of pitch changes, and then turned off the speaker. Either way is OK, but
the approach used by the sound() function is more versatile for our
purposes.
        The program SOUNDS contains a few sample audible signals selected via
a simple menu. You can use these as a starting point and create some of
your own.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 152 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        Don't forget to check the user's preference before "making a joyful
noise." Give the user a command-line argument to disable sound: Use -s, for
example, to set Silent to TRUE if it is Boolean, or to a nonzero value if
it is a simple integer. Then a simple conditional such as

        if (Silent == FALSE)
            warn();

will prevent the sound produced by warn() from being heard by an ungrateful
audience!


Getting User Input

        Next we turn our attention to the topic of getting input from the
user. In our programs, we sometimes need to ask the user for a filename, a
drive letter, or some other piece of important information. The goal of our
next task is to design a general purpose input routine that prompts the
user with a brief instruction and gathers a reply. It sounds simple enough:
Just use printf() to display the prompt and scanf() to collect the
response. This may sound reasonable, but this approach has some unnecessary
limitations and some really serious problems.
        The scanf() function is not appropriate because it is designed for use
with formatted data--the kind that is produced automatically by a program,
not entered by hand. Besides, users are notorious for breaking things,
accidentally or on purpose, and scanf() can be easily broken, even by the
well-intentioned user who simply makes a typing mistake. We need something
more versatile.
        Programs that have a lot of information on the screen may run short of
screen space, so to make things interesting, we will require that the
response field be able to take a response that is larger than the
displayable field width. The response field must, therefore, be a window
onto the user's response and must scroll sideways to permit the user to
view and edit the response text.
        Figures 6-5A and 6-5B depict what we are trying to do in terms of
screen presentation. We will use the BIOS video routines developed in
Chapter 5 to control screen appearance and the DOS keyboard routines
(also Chapter 5) to gather the user's input. Editing operations include
the usual destructive backspace (<-) and single-character delete (Del). A
character is inserted into the buffer by pushing all characters from the
cursor to the end of the line, to the right one position, and by putting
the input character in the vacated spot. The cursor moves to the right one
position.


            prompt field      reply field
            ┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐
            │░│░│░│░│░│░│▒│▒│▒│▒│▒│▒│▒│▒│▒│▒│▒│▒│▒│▒│▒│▒│▒│▒│▒│
            └─┴─┴─┴─┴─┴─┼─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┤
                        │                                     │
LINEBUF              └───┐                                 └───┐
headers  line buffers    │                                     │
┌─┬─┬─┐  ┌─┬─┬─┬─┬─┬─┬─┬─┼─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┼─┐
│■│■│■┼─►│▒│▒│▒│▒│▒│▒│▒│▒│▒│▒│▒│▒│▒│▒│▒│▒│▒│▒│▒│▒│▒│▒│▒│▒│▒│▒│▒│▒│
└┴┬┴─┘  └─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘
    │ │
┌┼┬▼┬─┐  ┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐
│■│■│■┼─►│▒│▒│▒│▒│▒│▒│▒│▒│▒│▒│▒│▒│▒│▒│▒│▒│▒│▒│▒│▒│▒│▒│▒│▒│▒│▒│▒│▒│
└─┴─┴─┘  └─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘

        FIGURE 6-5A ▐ Memory and screen of getreply()



TSTREPLY                                    File: c:\book\06user\timer.c
───────────────────────────────────────────────────────────────────────────

        FIGURE 6-5B ▐ Sample screen: Getting a pathname


        Cursor motions include moving to the beginning of text (Home), to the
end of text (End), and left and right by one character (left and right
arrows). Pressing Esc aborts the input operation and causes getreply() to
return a NULL pointer to the calling function; pressing Enter anywhere in
the line returns a pointer to the reply string. The calling function is
responsible for validating the reply it receives.
        If getreply() is to be used more than once for a given reply field in
a form or an interactive program, it would be wonderful if it could recall
the most recent response, or better yet, scroll back and forth through a
list of previous responses. Rather than put all of the complications
associated with list management into getreply(), we will have the calling
function pass a pointer to an array of line buffers that it sets up. Then
getreply() permits the user to scan up and down the list of line buffers by
using the up and down arrow keys.
        This design may seem to be a tall order to fill, but it is actually
not that difficult. Several cooperating functions give us a very nice user
interface routine and teach us a few things about windows and editing, too.
We will name the routine getreply() and try to generalize it enough to make
it useful in many programs.
        We will call on the standard library to help us with a few tasks. The
memory function, memcpy(), copies strings within a single segment. The
source and destination strings will be allowed to overlap to permit
sideways scrolling. The memcpy() function guarantees that no characters
will be lost in an overlapping copy operation. The reply string buffer
within getreply() is initialized to null bytes (\0) before it is allowed to
receive any user data. This action guarantees that the resulting string
will be properly terminated.
        After getting a preliminary version of getreply() working and grousing
over a few of its idiosyncracies, I arrived at the following sources for
the function, which behaves rather well when challenged by even the most
hostile users. The line buffer data structure is defined in linebuf.h, and
the source for the getreply() function is in getreply.c.

    /*
    *  linebuf.h
    */

    typedef struct lb_st {
            struct lb_st *l_next;
            struct lb_st *l_prev;
            char *l_buf;
    } LINEBUF;


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 157 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        A few parts of the code appear to be a bit dense and need some
explanation. The dimension of the reply field is calculated by subtracting
the prompt field width from the working area allowed to getreply() by the
calling function. Values are not error-checked, so be sure to pass
something that makes sense.
        The bulk of the work is done in the while statement that loops as long
as the Return key (or Enter as IBM prefers to call it) is not pressed. If a
key is a printable ASCII character, it is inserted into the buffer. If not,
it is checked to see whether it is an editing or cursor movement command.
Valid commands are executed and invalid keypresses evoke an error response.
        Controlling the window position relative to the input text is handled
by a pair of pointers. The character pointer cp tracks the current
input/editing position in the buffer, and wp, also a character pointer,
points to the first character in the buffer that will be displayed in the
reply field window. A test within the loop assures that the editing/input
position is always within the displayed reply window.
        The getreply() function uses an external error message routine. This
practice assures the calling routine that nothing will be displayed
arbitrarily on the screen. The calling function has complete control as to
when and where the error message will appear.
        One thing that angers me when I use some programs is having old error
messages still displayed on the screen. The code for getreply() prevents
messages from being displayed past their time. A message flag is set when a
new error message is issued and is cleared with the next keystroke. The
message handler is sent a null message, which is its hint to clean up the
message display area. It can ignore the hint, but it usually should not.
        To see getreply() in action, compile and link TSTREPLY, and run it.
Although the program does little more than call getreply(), it gives us a
way to test getreply() and demonstrate its operation. It also provides all
of the code necessary to set up a message handler and manage a line buffer
array. The C source is in tstreply.c:


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 161 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝



Chapter 7  Automatic Program Configuration



        We saw in Chapters 3 and 6 how programs can be given optional
arguments to control their behavior in order to meet special needs. In the
case of CAT in Chapter 6, for example, the -s option causes CAT to remain
silent about missing files. The use of such a command-line option is a
manual means of configuring a program.
        However, the use of command-line options is often an impractical
approach, particularly if a program uses many options. Users often will
avoid reading the manual page and therefore may give up on a perfectly good
program. What can be done about this?
        One solution is to treat an additional program feature as you would an
extra nose on your face--avoid it if you can. No sense cluttering up a
simple and effective program with lots of little gargoyle-like appendages.
Try to keep a program simple and geared to doing a single task as well as
possible. If you believe that a program needs another feature to round out
its capabilities, see whether other users of the program agree. But avoid
the temptation to put in every feature requested by every user; the result
will usually be a program that nobody uses.
        Assuming that there is no way to avoid adding that slick new feature
to your program, let's at least look at ways of automating things to make
using your software product easier for the "liveware." We can provide
automatic program configuration in several tidy, user-controlled ways,
including, but not limited to, using the program name and using external
configuration files.


Using the Program Name

        As we learned earlier, the name used to invoke a program under
DOS version 3.00 and later is available to the program. We could,
therefore, use a program's name to alter its behavior. For example, if some
additional code were put into our CAT program from the previous chapter, we
could cause the program to become a "silent" CAT simply by making a copy of
CAT.EXE with the name SCAT.EXE. Then the command

        C> scat filename

would have the same effect as the command

        C> cat -s filename

used in our current design.
        All we need to do is add these lines to cat.c just after line 46,
following the optional argument-processing loop:

        if (strcmp(pgm, "SCAT") == 0)
                    Silent = TRUE;

        Many other programs lend themselves to this form of configuration.
Under UNIX and XENIX, giving a program another name adds only a link to the
original file. No additional space is required for the program file's
contents. However, there is a price for doing this under DOS. Every
additional name for the same program file requires an additional directory
entry, a file allocation table entry, and storage space. Using aliases,
then, is not recommended except in rare situations in which the convenience
outweighs the lost disk space.


Using Configuration Files

        There is another convenient way to configure a program. Like the use
of the program's name, it too provides automatic operation. Although it
also requires additional disk space, it typically uses only a single
cluster, not a complete duplication of the original program file.
        In this method, variables and values are read from a file and are used
to initialize or update selected program variables at any time during the
operation of the program. Several levels of configuration files may be
used. An arrangement that works well in practice uses a global data file
that is located in a directory pointed to by a DOS environment variable.
        For example, on my hard-disk-based systems, I use a single
subdirectory called c:\config as a convenient location for all
configuration files for the programs that accept external configuration.
Each of these files bears the name of the program it configures plus a .CNF
extension. Thus, c:\config\progname.cnf contains the configuration data for
the PROGNAME program. The DOS variable CONFIG is defined by the statement

        set config=c:\config

in my AUTOEXEC.BAT file.
        A local configuration file--one located in the current directory--
may override the global file. This permits directory-by-directory control
over program behavior. For instance, a user of a program that prints the
contents of a text file may want one type of behavior for program source
files and quite another for a "dumb-things-I-gotta-do" list.
        The pseudocode for handling the configuration layers just described is
as follows:

            initialize program control variables to built-in values
            if (local configuration file is found)
                    overlay defaults with local configuration values
            else if (specified DOS variable found)
                    if (global configuration file found)
                            overlay defaults with global values
            else
                    return NULL file pointer
            update variables per command-line instructions, if any

        It is not an error for either the local or the global configuration
file, or both, to be missing, but it is an error for program-control
variables not to be initialized by the program before they are used. The
responsibility for the first step in the process--providing default
initialization--is left to the main program. The local and global steps can
be performed by a new addition to our utility library, the fconfig()
function. We still permit command-line arguments to override other settings
on a per-invocation basis, as indicated by the last step in the process
described by the pseudocode.
        Here is the C source code for the fconfig() function, which seeks a
configuration file and, if it finds one, returns a FILE pointer to the
file, which is open and ready to read from the top.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 168 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        The calling function must specify the name of an environment variable,
which may contain a global-configuration directory name, and the name of
the configuration data file itself.
        The name of the DOS variable that may hold a configurationdirectory
pathname is passed as a parameter to fconfig(), not hard-coded into the
function. This gives a calling program the latitude of specifying any
environment-variable name it wants. The pname string variable within
fconfig() is large enough (MAXPATH plus room for a terminating null byte)
to hold the longest pathname permissible under DOS.
        The environment variable, if it exists, holds the pathname that
represents the chain of directories leading up to the place where the
global configuration file would be located. The fconfig() function still
must append a backslash and the filename to complete the full pathname of
the configuration file. (Because the backslash is the C "escape" character,
two backslashes are needed to get one out, the first turning off the
special meaning of the second.)
        Nothing we've done so far places any restrictions on the type of data
that can be stored in and read from the configuration data file. The best
format is highly dependent on the amount of data and what will be done with
it. If a lot of Boolean flags are used to control a program, the best
approach might be to use a binary format with all bits significant. If the
data will be typed in by a casual user, the best approach might be to use
strictly ASCII text. In practice, it turns out that the ordinary text file
is best because it is more easily transported from one system to another
without concerns about the native size of data words or the ordering of
bytes within data words.
        All data read into a program from a configuration file should be
qualified in some way before being used to update program data. Data files
created by mere mortals often will contain errors, such as typos, incorrect
data types, out-of-range values, or too many or too few values. Data files
created by a custom-designed setup program are less prone to such problems;
however, checking all data wastes little time compared with the time it
takes to reboot a system that chokes on its input.


Printer Control Functions

        Now we'll look at an example of program configuration that nearly
every PC user has a need for at one time or another. In this section of the
chapter, we will develop a set of printer-interface routines that are aimed
primarily at controlling the fonts of Epson and IBM dot-matrix printers.
Then, in the next section, we will use the printer routines as the basis of
a printer-control program that enables us to control the printer from the
DOS command line and from batch files.
        Let's begin with a look at Epson and compatible printers, including
the original IBM PC printer. Many other printers use Epson-compatible
control codes, so the following routines are applicable without change to a
wide range of printers. For those printers that are not Epson compatible,
our interface accepts user-supplied configuration data from a file.
        To simplify matters a bit, we will make a few rules. First, the
printer must be able to produce emphasized text without having to backspace
and retype the text repeatedly. Second, the printer must be able to
underline without having to back up and restrike the text with underscores.
Third, if the printer does not offer a particular mode (double-strike,
compressed, or expanded, for example), initializing the control codes for
the mode to null strings ("") must effectively turn off the mode in the
interface routines; no attempt is made to synthesize a mode from
combinations of other modes.
        Limitations such as these would be deadly to a commercial printer-
control program, but they are acceptable here in our demonstration of
printer control because they simplify our task and make it more manageable.
If you need something more elaborate, you can use this program as a base
and add controls for paper length, top-of-form position, line-to-line
spacing, and many other features. The principles are the same as for the
font-control set described here.
        The file printer.h in the \include\local directory contains the
default values for Epson MX/FX-series printers and a set of printer
variables used by the interface routines.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 170 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        The ASCII codes are used directly to control printer modes (SO, SI,
and so forth) or to build up control-code strings (ESC in conjunction with
other characters).
        A set of font-type constants is defined so that each font type is
mapped to a single bit. This permits fonts to be "stacked" to form
composite fonts by using the bitwise OR. For example, ITALICS | UNDERLINE
combines the values 0x10 and 0x20 to request an underlined italic
font.
        The file printer.c contains a set of three related printer-control
functions: setprnt(), clrprnt(), and setfont(). These functions use the
variables defined in printer.h to determine what control strings to send to
the printer. Here is the text of printer.c:


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 172 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        The setprnt() function has the Epson default-control strings hard-
coded into it. However, if the user wishes to use some other printer,
setprnt() calls fconfig() to look for a local or global printer.cnf file to
override these values. If a configuration file is found, each line is read
in and assigned to the next printer control-code variable. If too many or
too few codes are obtained, an error is indicated by a return value of -1.
A return value of 0 tells the caller all went well, but it is still
possible that the codes were received in the wrong sequence or that bad
codes were given in the configuration file. The setprnt() function cannot
detect such errors; the printer will simply not operate correctly if it
gets bad control strings.
        The next function, clrprnt(), resets the printer to the normal font.
It does so by turning off each special font individually rather than by
using the hardware reset-control string. The hardware reset on most
printers resets the top-of-form and causes the paper to creep a bit each
time it is called. Because of this, I have elected to avoid using the reset
command entirely, although its value is contained in the configuration data
structure for possible future use. The clrprnt() function calls upon the
standard library function fputs() with the control strings needed to turn
off each printer font mode. It sends the strings to the output stream
specified by its only argument.
        The setfont() function accepts two arguments: one specifies the
desired font combination, and the other specifies the destination stream.
The first thing setfont() does is call clrprnt() to cancel all currently
active print modes. Then it issues control strings for each of the desired
modes.
        Most print-mode combinations are legal, but a few are not. In this
design, two font combinations are excluded. The setfont() function returns
a FAILURE indication if the caller requests the compressed mode combined
with either a double-strike or emphasized mode. Epson printers don't allow
these, but some other printers might. All other combinations of supported
fonts and print modes are permitted.
        The control strings are emitted one at a time for each requested mode.
Thus the request

        setfont(EMPHASIZED | EXPANDED);

in a program results in the control string for emphasized mode being sent
to the destination stream, followed by the control string for expanded
mode.
        To permit ready access to the printer interface, we will compile it by
typing

        msc print;

and then add the print.obj file to our utility library using the
command

        lib\lib\local\util +print;

        Now we can proceed to an example of how to use this simple but
effective printer interface.


        Printer Control Program

        My original purpose in designing the printer interface just described
was to gain some control over the printer from within my C programs.
(Chapter 9 is devoted to a general-purpose printer program that uses this
interface.) The approach I took gave me reasonable printer control from C
programs, and, with the small amount of extra work that we'll do next, from
the DOS command line and from batch files as well.
        We now have the tools needed to control print modes, but getting
access to the printer from DOS involves one additional step: packaging the
basic control functions in a DOS program file that accepts option flags to
set desired modes. In addition, we will write a supplementary program that
allows us to send arbitrary text strings to the printer.
        The MX program, originally named for its use with my trusty old MX-80
printer, controls the basic print modes via command-line arguments. The
program also works nicely with the Epson FX- and JX-series printers and
with many other compatible printers. The configurability of MX permits its
use with many other printers, too. A look at the manual page (Figure 7-1)
and source listing for MX shows that it is simple to use and equally simple
to program.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the figure found on page  ║
                ║ 177 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝

        FIGURE 7-1 ▐ Manual page for MX


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 178 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        Each time MX is invoked, it checks for a configuration file. This can
take a little time on a floppy disk-only system. If you will be using MX
with nothing but Epson-compatible printers on a floppy-based system, you
may want to bypass the configuration step and build in the Epson default-
control strings for faster execution.
        MX uses getopt(), which is now part of our utility library, plus all
of the printer-interface routines in the printer object file. Most of the
option flags are obvious; however, two require some explanation. The -o
option takes as an argument the name of a destination stream, presumably a
disk file, to be used in place of the standard output stream. This allows
us to capture the output of MX for diagnostic purposes or to create files
that can drive the printer directly as formatting scripts.
        The -t option uses the ASCII formfeed character to eject a page (go to
top-of-form). This will work on nearly all modern printers. To accommodate
all printers, you may instead want to permit the use of a series of
newlines to get to the top of the next page. The PR program described in
Chapter 9 shows how to use either control mechanism.
        The text of mx.mk tells the Microsoft MAKE command how to assemble the
MX program.

    # makefile for mx program

    LINC=c:\include\local
    LLIB=c:\lib\local

    mx.obj:         mx.c $(LINC)
            msc $*;

    mx.exe:         mx.obj $(LLIB)\util.lib
            link $*, $*,, $(LLIB)\util;

        The PRTSTR program is an auxiliary program that prints an arbitrary
string. PRTSTR also permits optional newline suppression, which gives users
the ability to construct printed lines in a mixture of print modes to
obtain various textual effects. The default for PRTSTR is to end the string
with a newline, which causes the printer to return to the beginning of the
current line and then move down one line. In addition, if it receives no
arguments, PRTSTR issues a single newline. Think of PRTSTR as an
intelligent substitute for the DOS ECHO command. The only advantage that
ECHO has over PRTSTR is that ECHO is built into the DOS COMMAND processor,
so it operates faster. However, because printers tend to move like molasses
in the Arctic, PRTSTR poses no problem in typical uses.
        The manual page for PRTSTR (Figure 7-2) describes its
operation and application. The source code and makefile (prtstr.mk) are, by
now, quite predictable.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the figure found on page  ║
                ║ 183 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝

        FIGURE 7-2 ▐  Manual page for PRTSTR


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 184 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        This very simple program has only one wrinkle that requires close
examination. When the newline-suppression option is selected, we suppress
not only the trailing newline, but also the single space that follows the
last argument string. This permits us to mix print modes within text
strings. We could, for example, print part of a word in boldface and
another part in italics to distinguish the fixed and variable parts of a
user's response.

    # makefile for prtstr program

    LINC=c:\include\local
    LLIB=c:\lib\local

    prtstr.obj:     prtstr.c $(LINC)
            msc $*;

    prtstr.exe:     prtstr.obj $(LLIB)\util.lib
            link $*, $*,, $(LLIB)\util;

        The SAMPLE.BAT file is a demonstration of printer control from a DOS
batch file. The sample text (Figure 7-3 on the next page) shows
off some of the many text combinations that can easily be obtained with
simple commands. Note the use of mixed print modes in some of the
lines.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 186 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║    Figure 7-3 is found on page 186       ║
                ║    in the printed version of the book.   ║
                ║                                          ║
                ╚══════════════════════════════════════════╝

        FIGURE 7-3 ▐  Output from SAMPLE.BAT


        Please keep in mind that this is a minimal printer interface intended
to demonstrate means of automatic program configuration. Modern dot-matrix
and ink-jet printers offer many additional font and print-mode selections,
and laser printers permit even greater selections. Have fun making the most
of them!


Selection Functions

        In configuring programs, some types of input require fairly elaborate
processing to get the input data into an internal form that is suitable for
program use. For example, the file select.c contains several functions that
work together to manage input data presented in the form of lists.
        The next section of this chapter uses the SELECT package to process
tab settings. Our objective is to take the user-supplied list of columns
that represents tab stops and convert it into an array of integers that can
be used by a program to initialize its internal tab stops to something
other than the hardware tabs that occur at multiples of eight columns,
starting in column one (1, 9, 17, and so on). (Users generally count the
leftmost column as one, not zero.) The SELECT routines can be used to
process other types of lists, too. For instance, the routines will be used
in Chapter 9 to select pages to be printed by the PR program.
        By way of example, one of our programs might receive a list such as 6,
20, 71--80 as a command-line argument. To be used by the program, the data
must be extracted from the list, which is a character string, converted to
integer form, and stored somehow for easy access. We'll deal with internal
data access soon. For now, let's just worry about the process of extracting
and converting the data.
        The example specification shown has twelve members. Technically
speaking, each entry in the list represents a range, although some ranges
have the same minimum and maximum values. Thus the 6 entry may be
represented internally as min = 6, max = 6, whereas the 71--80 entry is
represented as min = 71, max = 80. We will use an array of structures to
hold these values in data storage that is private to the routines within
the SELECT module.
        The SELECT module contains three related functions: mkslist() creates
a selection list from user data; save_range() is an internal function
(static) called by mkslist() to extract starting and ending values from
explicit and implicit range specifications; and selected() returns a
nonzero value to its caller to indicate that a given value is contained in
the  selection list.
        The routines in the SELECT module communicate through a global data
structure, Slist, which is defined as

        struct slist_st {
                long int s_min;
                long int s_max;
        } Slist[NRANGES + 1];

This allocates an array of NRANGES + 1 structures, each of which can hold a
minimum and a maximum value for a single range. NRANGES is currently
defined to be 10, but any reasonable number may be used. I have had no need
to specify more than 10 ranges in a list. Slist is declared globally here
for testing and demonstration purposes, but in practice it should be
declared static so that only the functions in the SELECT module can access
it.
        The pseudocode description of mkslist() shows how a selection list is
parsed. The manifest constant BIGGEST is defined in \local\std.h to be the
largest number (65535) that can be stored in an unsigned integer. A global
variable, Highest, is initially 0 and is updated to the highest value
received in the list.

    if (list in null)
            set min = 0
            set max = BIGGEST
    else
            while (next token of list is not NULL)
                    save_range(token)
            set min in next to -1 (terminate list)

        The save_range() function could be coded right in mkslist() because it
is only called once. However, by separating out the task of converting a
string token to a range of numbers, the apparent complexity of mkslist() is
reduced. The save_range() function is described as follows:

    copy first number to a buffer
    convert to long int
    if (only one number received)
            set max = min
    else
            if (second number is null)
                    set max = BIGGEST
            else
                    copy second number into buffer
                    convert to long int
                    save in max
    return (max)

        This design lets the user specify open ranges such as 40--, which
gives a firm minimum number but lets the maximum default to a large number.
This is handy for telling a program to operate on all lines starting at 40
and continuing until the last line (of unknown number) is reached.
        To determine whether a number is a member of the selection list, we
can call the function selected(), which returns a nonzero value if the
specified entry is a member of a range in the list. The selected() function
scans the selection list (Slist) one array element at a time until it finds
a range that contains its argument or until it finds a -1 flag that
terminates the list.
        The source code for the selection functions is contained in
select.c:


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 189 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        In mkslist(), strtok() is called with a pointer that is initially set
to the start of the user-supplied list, to extract the first (possibly the
only) token. The pointer is then set to NULL for subsequent calls to
strtok() to extract additional tokens, if any, from the list. Acceptable
separators are comma, space, and tab. This gives the list-maker
considerable flexibility in forming the list string. If the list contains
embedded spaces or tabs, it must be quoted, so that DOS sees it as a single
argument. A range specification must not have any white space around the
hyphen (-), so that strtok() can parse it as a single token.
        TSTSEL is a program that I used to debug the SELECT functions. It
accesses the Slist data structure directly, so that we can see what gets
stored in there for various list specifications. The operation of this
program is straightforward.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 192 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        The TSTSEL program produced the following output for one of my test
runs invoked with the command line

        tstsel 1,2,3,5--10


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 193 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        The first line shows the argument containing the user's list. The next
block of lines displays the contents of the select list, Slist. Note the -1
sentinel that marks the list's end. The next block of lines shows the
results when selected() is used to query the select list to determine if a
value is a member of a range. We have shown the results as YES/NO
strings.


        Setting Tab Stops

        In processing files that contain text, it is sometimes necessary  or
desirable to alter tab-stop positions, or to convert tabs to spaces or the
reverse.
        The formula for determining regular tab-stop positions is 1 + kn,
where k is the requested interval and n is an integer in the range of 0
through some maximum number determined by the line length. Thus, for the
value k = 8, tabs fall at 1, 9, 17, and so on.
        Variable tab stops are a bit more difficult to set up, but they have
their uses. We might appreciate being able to set the tab stops for a
FORTRAN program source file to 1, 7, 11, 15, 19, and 23, for example. We
can apply the selection-list technique just described to the setting of tab
stops at variable column positions in a line.
        The file tabs.c contains the source code for three functions used to
set and test tab stops. A private array of characters holds the tab-stop
data. Each character represents a column position in a line and contains 1
if the associated column is a tab stop and 0 if it is not.
        Two of the functions in the TABS module are used to set the tab stops.
The fixtabs() function installs tab stops at regular (fixed) intervals. The
interval is given as an integer argument to fixtabs(). The vartabs()
function works from a list to set tabs at variable intervals. We use the
SELECT functions to gather data from a user-supplied list and then convert
it to the needed form. A third function, tabstop(), is used to query the
Tabstops array.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 195 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        The TABS module uses C's natural base of 0 as the first element of an
array; therefore, all calculations are based on 0 as the leftmost column.
We accommodate the difference between the TABS module's view of the world
and the user's view of the world in the programs that handle the collection
and presentation of tab data.
        The best way to see how this all works is to demonstrate it. The
showtabs.mk file tells MAKE how to put it all together. The SHOWTABS
program gets a user specification of the needed tab stops. A -f option flag
means SHOWTABS is getting a fixed interval specification, and a -v option
flag signals a variable tab list. These options are mutually exclusive, and
the parsing of the command-line options enforces the use of only one of
them.

    #makefile for SHOWTABS program

    LLIB=c:\lib\local

    tabs.obj:        tabs.c
            msc $*;

    select.obj:      select.c
            msc $*;

    showtabs.obj:    showtabs.c
            msc $*;

    showtabs.exe:    showtabs.obj select.obj tabs.obj $(LLIB)\util.lib
            link $* select tabs, $*, , $(LLIB)\util;


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 197 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        If SHOWTABS receives a variable list, it calls the SELECT functions to
do the necessary conversion from the string form of the program argument to
an array of integers that vartabs() understands. In the case of a -f
option, SHOWTABS uses atoi() to produce a single number, which is taken to
be a fixed interval specifier that is passed to fixtabs().
        Once the Tabstops array is populated, SHOWTABS calls upon tabstop() to
display the status of every column in the first 80 columns of a line. A
plus sign (+) in the display marks columns that are nonzero multiples of
10. A T marks a tab stop, and a minus sign (-) marks all other positions.
Figure 7-4 on the next page shows the results of various tab-setting calls
to SHOWTABS.


C> showtabs -v 1,9,17,61-68
T-------T+------T--+---------+---------+---------+---------+TTTTTTTT-+


C>

        FIGURE 7-4 ▐  Output from SHOWTABS


        Because the functions in both the SELECT and TABS modules are
generally useful, we will add select.obj and tabs.obj to util.lib in
\lib\local.
        In the next chapter, we will develop a group of utilities for DOS that
emulate some of the UNIX utilities; this will help ease the transition of
new users from one system to the other and will help those who are
constantly moving between the two operating systems, as is the case for
many software developers.



SECTION III  File-Oriented Programs



Chapter 8  Basic File Utilities



        This chapter presents a set of file-oriented utilities that satisfy
two primary goals. First, the commands give programmers who divide their
time between UNIX/XENIX and DOS a set of programs that make it easier to
switch back and forth between the two environments. The programs
essentially implement a UNIX/XENIX-style interface under DOS. Second, the
programs serve as models for other utility programs that you might like to
add to your toolkit. The programs employ many of the functions and
techniques we have developed thus far in the book plus a few that are
new.
        The programs in this set include the following utilities:

═══════════════════════════════════════════════════════════════════════════
    UTILITY                  ACTION
───────────────────────────────────────────────────────────────────────────
    TOUCH                │   update the modification times of files
    TEE                  │   split a stream into two separate streams
    PWD                  │   print the working directory pathname
    RM                   │   remove files
    LS                   │   list the files in a directory
───────────────────────────────────────────────────────────────────────────

        Some of these programs perform functions already provided by DOS, but
in a way that is more familiar to UNIX/XENIX users. LS is similar to the
DOS DIR command. PWD does the job of the DOS CD command, when CD is typed
without an argument. The RM command provides some enhancements over the
standard DOS ERASE/DEL command. Other programs in this set are tools used
by programmers that have no DOS equivalents.


Update File Modification Time (TOUCH)

        We have been using the MAKE command to assist us in creating and
maintaining programs. The MAKE command uses a file's last modification time
and instructions in a "makefile" as the basis of its decision-making
process. MAKE compares the modification time of an object with that of its
sources (specified in a dependency list in the makefile) to determine
whether an object is older than one or more of its respective sources. So,
when we modify a source file, every object that lists that source file in
its dependency list will be remade.
        At times, we may want to force an object to be remade. We can do this
in several ways. We can edit a source file; even if we make no changes to
the file, the act of saving the file with an editor updates its
modification time. The source will then be newer than the object that is
created from it, so the object will be remade.
        Another way to force an object to be remade is to remove it. MAKE will
discover that the object does not exist and will remake it from the recipe
in the makefile.
        A third way is to use a program called TOUCH to update the file
modification time of the source file without making any other changes to
it. This will also cause MAKE to remake the object. TOUCH is particularly
handy when you want to force all the files in an entire project or
subproject to be remade. A single TOUCH command does the work of many
separate DEL or editor commands.
        The following pseudocode describes how TOUCH works.

            for (each named file)
                    update time
                    if (update not successful)
                            increment error count
                            if (verbose)
                                    print error message
                    else
                            if (verbose)
                                    print confirmation message

        The manual page for TOUCH is presented in Figure 8-1.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the figure found on page  ║
                ║ 207 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝

        FIGURE 8-1 ▐ Manual Page for TOUCH


        The source code for the TOUCH program is in the file touch.c.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 208 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        Note the use of two dummy functions, _setenvp() and _nullcheck().
These functions reduce the final program code size slightly by eliminating
the code for environment processing and for checking attempts to reference
data through null pointers. The _nullcheck() function is inside a
preprocessor directive block that includes the code during debugging and
omits it when the program is finally compiled.
        If we are really serious about minimizing code size, we will also
eliminate calls to printf-family functions because the formatting code is
rather large (about 2.5--8 KB, depending on the compiler and memory model
used). Using fputs() and fputc() to synthesize the fprintf() function, when
it is used to process only strings, will save several kilobytes in the
executable program. But be on the lookout for "hidden" formatting.
Depending on how the compiler supplier implements perror(), for example,
you may find that a call to fprintf() gets dragged in anyway, even though
you didn't use one directly in your code.


Tapping a Pipeline (TEE)

        The TEE program is often called the "pipefitters' dream." The manual
page for TEE (Figure 8-2 on the next page) tells you why.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the figure found on page  ║
                ║ 212 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝

        FIGURE 8-2 ▐ Manual page for TEE


        TEE always writes a copy of its input to the standard output stream.
In addition, TEE attempts to open for writing any files given as command-
line arguments. If the option -a is given, TEE attempts to open the files
in append mode instead of write mode. By naming output files, the user
creates a multi-way split and sends copies of the same input stream to two
or more output streams. The pseudocode for TEE is

            set file mode string
            for (each name file)
                    open file per mode string
            for each character received from stdin
                    copy the character to all open output streams
            close all streams

        TEE is especially useful in debugging programs. I use it to view the
output of a program on the screen while capturing a copy of the data stream
in a file for detailed inspection using DUMP (Chapter 10) or a text
editor. It is surprising what kind of garbage finds its way into the output
data stream of an ill-behaved program.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 213 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        The source code for TEE introduces the use of the standard library
function fcloseall(), which can be used to shut down all open streams
except the standard streams that were opened for you by the C run-time
startup system. The function returns -1 in the event that it cannot close a
stream. We report this to the operating system via the exit() function. The
ERRORLEVEL feature of batch files can be used to test the return value, but
DOS ignores it.


Print Working Directory Path (PWD)

        The manual page for PWD (Figure 8-3) says all you need to
know about "what" but may leave you wondering "why." What's wrong with
typing CD without an argument to find out the name of the current
directory?
        Nothing, really. But those of us who switch back and forth between DOS
and UNIX/XENIX have certain reflex reactions that cause problems. Under
UNIX/XENIX, a bare CD command reports nothing. Instead, it takes you back
to your "home" directory from anywhere in the directory hierarchy. The PWD
command is used to display the current directory pathname. I wrote a DOS
PWD command to make the two environments a bit more alike for some often-
used commands.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the figure found on page  ║
                ║ 215 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝

        FIGURE 8-3 ▐ Manual page for PWD


        The source for PWD, pwd.c, uses the standard library function getcwd()
to get the current "working" directory pathname. The first argument can be
the address of a user-supplied buffer that is long enough to hold a DOS
pathname (64 characters). If NULL is used instead, getcwd() creates its own
buffer, the length of which is specified by the second argument.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 216 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


Remove Files (RM)

        One of the limitations of the DOS ERASE/DEL command is that it accepts
only a single file specification; if we want to remove several different
types of files, a series of ERASE commands must be issued. Another
limitation is the lack of an interactive deletion feature. RM does the job
of ERASE while adding these two useful features.
        The manual page for RM is presented in Figure 8-4. The basic
behavior of RM is the same as ERASE except that you may provide any
number of exact and ambiguous filename specifications up to the limits
imposed by the DOS command line. Each command-line argument (following
command options, if any) is expanded if necessary. The -i option, when
used, puts RM into an interactive mode that is a great help in avoiding
the unwanted loss of files. Each deletion must be confirmed by a Y
response and a Return--more work for the user, but safer.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the figure found on page  ║
                ║ 217 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝

        FIGURE 8-4 ▐ Manual page for RM


        The pseudocode for the heart of RM is

            for each file
                    if (interactive mode selected)
                            print file name
                            prompt user for reply (y/n)
                            read a line of input
                            if (no)
                                    break
                    else
                            unlink file
                    if (error occurred)
                            print error message

        The source for RM is composed of three functions: main(), do_rm(), and
affirm(). The return value of the call to unlink(), a standard library
function, is compared to -1, which flags an error condition. The perror()
library function is used to print an error message. There is no exit made
at this point because other files may still need to be removed.
        The most likely error is an attempt to remove a non-existent file.
Also likely is the typical failure caused by attempts to access a floppy
drive that has no disk or has its door open. RM simply reports about non-
existent files; it lets the DOS error handler deal with drive problems.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 218 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        That's it for the easy utilities. The next is more powerful and useful
than the utilities we have just written and is appropriately more
complicated and more difficult to write as well. As the weight lifters say,
"No pain, no gain."


List Directories (LS)

        Let's start with the manual page for LS (Figure 8-5 on the next
page); it shows us where we are headed. Note that this LS command has
six options. That may sound like a lot, but the UNIX/XENIX ls command has
more than three times as many! To keep things simple and usable, I elected
to leave many bells and whistles out of LS. Anyone who wants another 15
options is free to put them in.
        The pseudocode for LS only hints at the complications of doing this
job in a DOS environment. Ignoring startup tasks (version control, option
parsing, setting up a Ctrl-Break handler, and so forth), LS follows this
general blueprint:

            allocate a buffer for file data
            allocate a buffer for directory data
            if (no arguments)
                    get current directory pathname
            else
                    for each named item
                            if (it's a directory)
                                    add directory name to directory buffer
                            else if (it's a file)
                                    add file name and data to file buffer
            if (any files in file buffer)
                    sort entries
                    send file list to output
            if (any directories in directory buffer)
                    for (each directory)
                            expand it to a list of files
                            sort entries
                            output list

        The LS program resides in several files that are devoted exclusively
to LS-related chores. In addition, several DOS functions of more
farreaching application are used. These are new, and will be added to the
local DOS library that we created in Chapter 5.
        The header file, ls.h, shows the data structures that describe file
data and the manifest constants needed to request and interpret file data.
Note that the LS program deals with information about files, rather than
the information that is in them.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the figure found on page  ║
                ║ 223 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝

        FIGURE 8-5 ▐ Manual page for LS


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 223 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        The OUTBUF structure template is used to allocate storage for an
output buffer that contains the name of a file, its modification data and
time, the file size in bytes, and the file mode for each file in the
default directory and any named directories.
        The basic substance of the LS program is in the file ls.c. It contains
a slew of include file directives, a set of global configuration variables,
the main() function, and a Ctrl-Break handler function. LS can produce
mountains of output; therefore, we must provide a convenient way to break
out if the user gets bored or finds the sought-after information part way
through the listing.
        The main() function orchestrates everything, calling on other
functions to do most of the hard work. Its job is to collect the user's
requests and preferences and then to parcel out the work of getting file
data, expanding directory names to the contents of the directories, and
much more. Here is the ls.c file. Think what it might look like with 15
additional options!


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 224 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        Global variables are used to hold control information that is needed
by other functions in the LS program. Global variables should not be used
to pass data between functions.
        The main() function calls on malloc() to allocate memory for the file
data structure and for an array of pointers to directory names. The
technique used here allocates a fixed amount. If that amount proves to be
too small to handle the job, additional space is obtained from realloc().
This guarantees that the expanded allocation is contiguous with the earlier
allocation (even if the whole block has to be moved to a new location in
memory), which makes it possible to use pointers and array indexes to
access elements in the data areas.
        Two DOS functions, getdrive() and drvpath(), are called by main(). The
first gets the current drive number (0 = A, 1 = B, and so forth), and the
second appends to a copy of the current pathname onto a drive
specification.

    /*
    *      getdrive -- return the number of the default drive
    */

    #include <dos.h>
    #include <local\doslib.h>

    int getdrive()
    {
            return (bdos(CURRENT_DISK));
    }


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 231 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        These two files should be compiled and the resulting object files
should be added to the dos.lib in the \lib\local directory.
        After the file and directory buffers have been filled, main()
sequences the outputting of the data. All file data goes out first,
followed by the file and directory names associated with each named
directory. The simplest case is a list of files produced by a request
like

        ls

which produces a one-column listing of the file and directory names in the
current directory. The output is done in a multi-column format if the
MULTICOLUMN variable is set to the value of logical 1 by using -C on the
command line or by renaming the LS program to either LC or LF under DOS
version 3.00 or later.
        The output of LS is directed to the standard output stream, so it
appears on the screen unless it is redirected.
        A default of 80 columns maximum is used to determine line length. This
fits easily on most display screens and printers. You might want to add a
feature that allows the line length to be controlled, either from the
command line or by a configuration file or an environment variable.
        Output lists are sorted lexically by default. Options allow the user
to select reverse sorting (-r) and sorting by last modification time (-t).
The standard library sort routine, qsort(), does the sorting and calls on
ls_fcomp() to compare items being sorted. The file ls_fcomp.c contains the
source for the comparison function.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 233 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        After lists are sorted, they are sent to the output device by
functions in the ls_list module. Single-column output is done by
ls_single(). Multi-column output is handled by ls_multi(). The latter
function is the more interesting one. First, here is the source:


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 234 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        The ls_multi() function receives a sorted list of names, scans it once
to find the length of the longest name, and then outputs the list in
columns. The number of columns depends on the file data. Optimal column
width and number of columns are determined by the length of the longest
filename or subdirectory name to be output, thus maximizing the number of
columns output and minimizing the number of lines used. All names are
output in lowercase. Most people find words in all uppercase letters
difficult to read; if you don't, feel free to leave out the call to
strlwr().
        The next function, ls_dirx(), is the workhorse to which falls the task
of expanding a directory name into a list of its subordinate file and
subdirectory names. This is not a trivial task. The file system under DOS
is fractured--part residing in a fixed directory at the "root" level of a
disk, and the rest in dynamically allocated subdirectories that are simply
special files in the file system. In addition, we must be concerned with
the use of a drive name (A, for example) as an alias for the current
directory on that drive. Note: The drive name is not an alias for the root
directory.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 238 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        To get the data it needs from DOS, ls_dirx() calls on three
DOS functions: setdta() to establish a disk transfer area in memory;
first_fm() to obtain the name of the first file that matches an ambiguous
filename specification; and next_fm() to get any subsequent matches to the
same specification. We will compile these three functions and add the
object modules to the DOS library.

    /*
    *      setdta -- tell DOS where to do disk transfers
    */

    #include <dos.h>
    #include <local\doslib.h>

    void
    setdta(bp)
    char *bp;       /* pointer to byte-aligned disk transfer area */
    {
            union REGS inregs, outregs;

            inregs.h.ah = SET_DTA;
            inregs.x.dx = (unsigned int)bp;
            (void)intdos(&inregs, &outregs);
    }


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 241 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        In addition, ls--dirx() calls last--ch() to examine the last character
in the pathname. If the last character is not a \, a \ is appended to the
pathname. The last--ch() function is added to the \lib\local\util.lib
library.

    /*
    *      last_ch -- return a copy of the last character
    *      before the NUL byte in a string
    */

    char
    last_ch(s)
    char *s;
    {
            register char *cp;

            /* find end of s */
            cp = s;
            while (*cp != '\0')
                    ++cp;

            /* return previous character */
            --cp;
            return (*cp);
    }

        To assist you in producing and maintaining the LS program, here is a
makefile, ls.mk:


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 243 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        The temptation to add features to LS is great. It (and its
equivalents, such as DIR) is a high-use program, typically accounting for
some 30 to 40 percent of command executions on a given system. Perhaps
because of its visibility, it is a frequent target for enhancements. Try to
avoid turning LS into a program that no one uses because it is too
complicated.



Chapter 9  File Printing



        One essential programmer's tool is a program that displays or prints
the contents of text files--program source files, debugging listings,
program "map" files, and so on. The PR program developed in this chapter to
take care of this task is patterned after the UNIX utility of the same
name. It is designed to be flexible yet not encumbered with unnecessary
features. It provides a basic file-printing capability supplemented by a
collection of helpful options.
        By default, PR produces output suitable for a generic line printer. It
uses only the standard format-effector codes (carriage return and linefeed)
and normal displayable characters (including space). The options enable you
to use special features of the Epson and Epson-compatible printers,
sequentially number lines, print a list of pages from a file, and control
basic page layout. Many options of PR allow the program to be used as a
filter, reading from its standard input and producing formatted pages on
its standard output or other output streams.


Program Specification

        Figure 9-1 on the next page is the manual page that
describes the features and applications of PR. During the development of
the program, the manual page served as a wish list. A prototype of PR,
based on an earlier version of the manual page, was tested on some
"friendly users." Their feedback led to changes in the manual page and
subsequent revisions of the program that eventually resulted in the final
program presented here.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the figure found on page  ║
                ║ 246 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝

        FIGURE 9-1 ▐ Manual page for PR


        I will describe the program's basic features and its options in detail
as the PR functions are presented, but before doing that, we need to define
just what is meant by a "page" of output.
        Figure 9-2 is a proposed page layout. The assumption that governs
choices of default values is that standard document paper is used. A
sheet of letter-sized paper is 8-1/2 inches wide by 11 inches long. (An
additional half-inch on either side of a sheet of pin-fed paper contains
the holes for the tractor feed, resulting in a raw page size of 9-1/2 by 11
inches.) A good guideline regarding page appearance is that at least 1/2
inch of unused space should be left on both sides and at the top and
bottom. Using less white space tends to make a printed page look too
crowded.


                left margin                        right margin
                ┌──────┬─────────────────────────────┬──────┐
                │      │▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒│      │║
        (TOP 1)─┼──────┼────►▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒│      │║
                │      │▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒│      │║
                │      │                             │      │║► header
                │      │▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒│      │║
        (TOP 2)─┼──────┼────►▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒│      │║
                │      │▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒│      │║
                │      │                             │      │
                │      │                             │      │
                │      │                             │      │
                │      │            text             │      │
                │      │            body             │      │
                │      │                             │      │
        (MARGIN)──┼──►   │                             │   ◄──┼──(MARGIN)
                │      │                             │      │
                │      │                             │      │
                │      │                             │      │
                │      │                             │      │
                │      │▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒│      │║
        (BOTTOM)─┼──────┼─►▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒│      │║► footer
        ┌──┐      │      │▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒│      │║     ┌──┐ ┌──┐
        │  │      └──────┴─────────────────────────────┴──────┘      │▒▒│ │  │
        │  │ print area                              non-print area  │▒▒│ │  │
        └──┘                                                         └──┘ └──┘

        FIGURE 9-2 ▐ Proposed page layout


        The left side of a page may need to be punched for ring binding, so
some additional clear space might be needed there. For most purposes,
however, I find that the default of 1/2 inch is enough. This indentation or
offset is obtained by spacing in five columns for a 10-pitch font.
Condensed type, such as that produced by an Epson printer in condensed
mode, requires a larger number of columns (about eight) to obtain the same
physical offset.
        Given the specification presented in the manual page and the values
obtained from the proposed page layout, we can now design a program to
produce nicely formatted listings of text files. We can start by noting
that PR follows a pattern of behavior that is similar to CAT: It takes some
input, applies some transformation, and produces some output. However, we
will have to treat lines a bit differently to accommodate optional line-
numbering, page offset, and other aspects of format.
        If our design is done well, we should have enough flexibility to
handle 90 percent of all the printing needs of a programmer. The behavior
of the PR program will be controlled by reasonable built-in defaults that
may be modified by configuration files located in the current directory or
in a directory selected by the user and by command-line options.
        After analyzing the requirements for PR and trying several simple
prototypes to investigate ways of implementing the features, I settled on
an organization for the program. Each program module is packaged in a
separate file, usually with one function per file. The program has the
following functions and calling hierarchy:

            main()
                    getpname()
                    pr_gcnf()
                            fconfig()
                    getopt()
                    pr_help()
                    pr_file()
                            pr_cpy()
                                    mkslist()
                                    pr_getln()
                                    fit()
                                    lines()
                                    spaces()
                                    setfont()
                                    selected()
                                    pr_line()
                    exit()

        As you can see, many functions that we developed in earlier chapters
are called by functions used in PR. These external names are resolved by
the linker, using util.lib as one of the searched libraries.
        If the user gives an erroneous option, main() calls pr_help() to
display an abbreviated manual page on the stderr channel. The C source for
pr_help() is:


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 250 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        The function uses a static array of string pointers to message lines.
The calculation in the line

        n = sizeof (m_str) / sizeof (char *)

sets n to the number of message lines in a way that lets us add message
lines without having to count them and declare how many are used. This
makes it easier to modify the function as the program evolves, as most
programs do.
        To achieve the desired flexibility, we will use a global data
structure to hold the values of the important formatting and program-
operation control variables used by the program's various modules. Only
those modules that need to know about the data will be given access to the
data structure. Here is the header file, print.h, that contains the data
structure definition and some manifest constants used to initialize the
program:


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 251 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        A typedef is used to create a synonym, PRINT, for the struct pr_st
data structure. The structure contains about a dozen integer-sized variable
definitions and two string variable definitions. The header file defines
the printer data structure, but it does not declare any storage for it.
That is done in the configuration function, pr_gcnf(), which we'll examine
shortly. First, let's take a look at PR from the top.
        The main() function is contained in the file pr.c. It declares and
initializes global variables, establishes the default output channel,
processes command-line arguments, and calls pr_file() with a list of files
named as input by the user. If no filenames are specified, PR uses the
standard input channel so that data can be routed through PR from a
redirected input or from a pipeline. Thus, PR fits the description of a
filter in the same way as the DOS programs MORE and SORT, except that PR
can be configured to send its output to someplace other than the standard
output. In such cases, PR is referred to as a sink instead of a filter,
because it is a termination point for data flow.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 252 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        The main() function calls getpname() to get the program name and then
calls pr_gcnf() to get the configuration data needed to initialize the
program. Next, it uses getopt() to process user-supplied options, if any,
and finally it passes a list of filenames (possibly none) to pr_file(). We
have seen the use of getpname() and getopt() before. There should be no
surprises here.


Option Handling

        The pr_gcnf() function is specific to PR, but the design can be
applied to other programs. The pseudocode for this function is

            if (a configuration file exists)
                    open the file
                    read numeric data
                    read string data
                    if (data good)
                            set flag
                    close file
            if (flag is set)
                    initialize to values just read
            else
                    use defaults from the header file

        Here is the source code for pr_gcnf():


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 255 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        The PRINT type is used to declare a global data structure for the
printer, pcnf, which stands for printer configuration. Any other source
file that needs access to pcnf must declare it as external data.
        If no configuration file is found, pr_gcnf() falls through to the
default assignments. The fconfig() function we created in Chapter 7 is
used to query DOS for an external configuration file. A NULL result
indicates that none was found. If a configuration file is found, it is read
in a line at a time.
        Numeric values are obtained by converting the character
representations in the file to integers by using the standard library
function atoi(). Because atoi() accepts only characters that can be
considered part of a number, we can add comments to the lines. Thus, when
the line

        66 number of lines per page

is retrieved from the configuration file and is given as the string
argument to atoi(), the function merely assigns the integer value 66 to a
temporary array.
        String arguments are handled similarly. A line is read in and the
library function strtok() extracts the first token, which is the string
value we need to save. The remaining characters on the line, if any, are
ignored.
        An example of a string variable is p--dest, which determines where
PR's output will go. This variable is a member of pcnf and is accessed
using the dot operator as in pcnf.p_dest. Each line of output is sent by
default to the device specified in p_dest. If p_dest is set to CON, or if
it is null (""), output is directed to the standard output channel and will
be displayed on the user's console screen unless program output is
redirected. (MS-DOS and UNIX permit the standard output of a program to be
redirected using the greater-than (>) symbol--see Chapter 3 or your DOS
manual for details. Thus, output that would otherwise be displayed on the
system console or a terminal screen may be sent to a printer port or
captured in another file.)
        In this presentation of the PR program, I have set p_dest to PRN, the
default printer device, because that is where I usually want the output to
go. We can still send the output to the screen by selecting the -p
(preview) option, which uses CON as the destination for a single invocation
of PR.
        To display the contents of a file named myfile.txt on the system
console, you would type

        pr -p myfile.txt

        To send the output to a printer that is attached to the default
printer port, you would instead type

        pr myfile.txt

        If PR is compiled with ssetargv.obj included in the linker object list
(as shown in the makefile, pr.mk), PR permits the use of wildcards in
filenames, letting you use abbreviated command lines to specify print jobs.
For example, to print hard copies of all the header files in the include
subdirectory and in the include\sys subdirectory, type

        pr \include\*.h \include\sys\*.h

        If you have a hardware or software print spooler on line (such as the
one provided with Microsoft Windows), you can turn your attention to
something productive while your printer dumps a mound of paper on the
floor. Otherwise, you'll have to mark time for a while.


File Handling

        The processing loop in pr_file() receives a list of files and tries to
get each file formatted and printed. The function must handle errors
gracefully. If, for example, a user specifies a file that does not exist or
one that cannot be opened, the program sends an error message to the
standard error channel and then continues looking for more files to print.
The standard error channel is used because messages sent to it will appear
on the console screen even when normal program output is directed
elsewhere. The standard input channel is used as a data source if no files
are named on the command line.
        The pr_file function is also responsible for connecting the output of
PR to the required stream. If the user has configured PR to send output to
one of the standard output channels (sdtout, stdprn, or stdaux), there is
no need to open the stream, because DOS has already done so when PR started
running. Other destinations, such as a named file, must be explicitly
opened for writing.
        The source code for pr_file() is:


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 259 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        Notice the message "Cannot stat (filename)" after the call to pr_cpy()
(described next). This tells the user that file statistics could not be
obtained for the named file even though it was found and opened. This flags
a possible error with the disk directory and should inspire the user to
find out why the error occurred. It could be that a bad sector is
developing or that a disk formatting or updating problem has occurred.


        What's My Line?

        The work of orchestrating the formatting and copying of text pages
falls to pr_cpy(). A line in a text file may be thought of as a single
logical line that may be represented as one or more physical lines of
output. Long lines of output--those that exceed the width of the printable
or displayable area--may be handled in at least three ways. We can:

        ►  let the line run long and leave the treatment up to the printer
        and its driver software. This approach can result in some
        bizarre looking output.

        ►  "fold" a long line so that what cannot be displayed or printed on
        the current line is moved to the next output line.

        ►  truncate the input so that anything on a line that would have to
        be displayed past the last column is effectively lost.

        The PR program has been designed to fold lines so that nothing is lost
on output. If line-numbering is turned on, a logical line number is
displayed at the beginning of each line. If a line has to be folded, as
shown in Figure 9-3, the additional physical lines needed to
print or display it are indented as usual, but they do not have a number
field associated with them. Logical lines are numbered starting with one.
The number of a line indicates its relative offset from the beginning of
the file.


            .
            .
            .
            46          /* process command-line arguments */
            47          while ((ch = getopt(argc, argv, "efgh:l:no:ps:w:")) !=
        EOF) {
            48                  switch (ch) {
            49                  case 'e':
            .
            .
            .

        FIGURE 9-3 ▐ A folded line, as displayed by PR


        Before going any further, here are the pseudocode and C source code
for pr_cpy():

            set up source and destination streams
            if (source is stdin)
                    get current time
            else
                    get file modification time
            for each line in source file or stream
                    if (line won't fit on page OR formfeed)
                            eject page
                    if (at top of a page)
                            print header
                    print the logical line
            if (not at top of page)
                    eject page


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 262 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        The pr_cpy() function handles a few other tasks in addition to fold-
ing long lines. It detects that an input stream has ended before the output
page is full and emits enough extra blank lines to get back to the top of a
fresh page. That's so the next file will start printing in the right place.
It makes sure that there is enough room remaining on a page to accept the
next logical line. The static function fit() within the pr_cpy.c file does
the needed calculations.
        The function also needs to handle a special situation that results
from a fairly widespread practice. Programmers often create source files
with two or more functions in them. Sometimes a literal formfeed control
character (Ctrl-L) is placed between functions. This causes most printers
to eject a page and to begin printing anew at the top of the next page.
Care must be taken to keep the file-relative line and page numbers
accurate.
        The functions mkslist() and selected() obtain a selection list and
determine whether an item is in the list; they were explained fully in
Chapter 7. These functions are called by pr_cpy() to process a list of
selected pages, the Pagelist variable, to be formatted and printed by PR.
The special page number BIGGEST is used to supply the high end of an open
range specification (like 20--), which causes PR to print all pages between
20 and the end of the file.
        The pr_getln() function obtains a line of text from the input stream
and expands it while reading it into the line array. The expansion alluded
to here is that of converting each tab character in the input into the
right number of output spaces so that column alignments are retained. The
expansion is necessary because we will be doing indenting (page offset) and
other formatting operations upon output, and because some older printers do
not handle tabs correctly. All positions in the line array that are used to
hold the text must be filled with characters that have a single unit of
displacement in the output. This facilitates character counting so that
fit() can determine how much space is required for the expanded line.
        This is the source code for pr_getln():


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 266 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        The tab expansion is done by the functions in tabs.c that were
described in Chapter 7.
        Top-of-page processing is done directly by pr_cpy(), which prints the
page header composed of some blank lines, a header line that contains the
name of the current file (or a substitute text string), the output page
number, and the date and time when the file was created or last modified.
The blank lines are produced by a call to lines(), a function that emits a
series of newlines to the specified stream.

    /*
    *      lines -- send newlines to the output stream
    */

    #include <stdio.h>
    #include <stdlib.h>

    int
    lines(n, fp)
    int n;
    FILE *fp;
    {
            register int i;

            for (i = 0; i < n; ++i)
                    if (putc('\n', fp) == EOF && ferror(fp))
                            break;

            /* return number of newlines emitted */
            return (i);
    }

        The filename as typed by the user is converted to uppercase. This is
done because DOS automatically converts filenames returned in response to
an ambiguous specification (using wildcards) in uppercase letters. We could
just as easily convert everything to lowercase.
        The formatting of the header line differs from the text body when the
Epson-compatible mode is selected. The setprnt(), setfont(), and clrprnt()
functions described in Chapter 7 are employed to control the printer so
that headers may be printed in a different typeface than the main text.
(Although not a necessary feature, this demonstrates a practical use of
different typefaces on a single page.) The text body can be printed in a
compressed form, to permit long lines (up to about 137 characters) to be
printed on standard letter-sized paper.
        The function pr_line() is called to output a logical line of text
composed of two or three parts: a left margin produced by a call to
spaces(), an optional file-relative line-number field, and finally the text
of the expanded line. If additional physical lines are needed, spaces() is
called at the beginning of each to obtain a uniform page offset.
        Here is the source code for pr_line():


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 268 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        The pr_line() function returns the number of physical lines that were
used to print the single logical input line.
        The spaces() function (on the next page) is almost identical to
lines(), except for the character that it emits (space instead of
newline).

    /*
    *      spaces -- send spaces (blanks) to the output stream
    */

    #include <stdio.h>
    #include <stdlib.h>

    int
    spaces(n, fp)
    int n;
    FILE *fp;
    {
            register int i;

            for (i = 0; i < n; ++i)
                    if (putc(' ', fp) == EOF && ferror(fp))
                            break;

            /* return number of spaces emitted */
            return (i);
    }

        Since the tasks performed by lines() and spaces() are needed
frequently in programs that send text to the screen or some other output
device, we will add the object files lines.obj and spaces.obj to our
utility library, util.lib, by compiling them and using the command

        lib util +lines spaces;

Remember to copy the updated library into the \lib\local subdirectory so
that LINK can find it.


Program Maintenance

        A special-purpose library called prlib.lib contains all but a few of
the object modules that comprise the PR program. The pr.obj file, which
contains the main() function, is kept separate from the rest because of the
way the DOS linker, LINK, works. The executable filename defaults to the
name of the first object module specified in the link list, and at least
one module must be named.
        The makefile script for PR is in pr.mk. It automatically keeps the
needed objects and the prlib.lib file up to date as changes are made to the
program source files. Here is the text of pr.mk:


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 271 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


Possible Enhancements

        As well endowed as PR is already, there is always some new wrinkle or
feature that some user would like to see. In trying to avoid making PR into
a program that serves everyone in every way but no one particularly well in
any way, I have chosen to stick with the feature set provided.
        However, some users of PR have requested the following features, which
you may want to consider adding. First, it might be helpful to have a
multi-column output feature. This would take lines from a file and present
them in columns across the page. Each line would be truncated if it
exceeded the column width minus one character position for a separator. It
would be necessary to format an entire page in memory before sending
anything to the output.
        Another feature of interest to some users is a more elaborate way of
controlling the appearance of the header and footer. This, to me, smacks of
word processing, and I have elected to use Microsoft Word if I need such
control over the output.
        The addition of single-sheet control might be useful to some users. I
always use pin-fed, fan-fold paper, so I have not felt a compelling need
for this feature. It can be added easily by introducing a new numeric
(actually a Boolean) variable in the PRINT data structure and testing it
before outputting a new page.
        One additional feature may make it into my next version of PR. Some
users would like to be able to do side-by-side printing of multiple files.
This is a kind of visual file comparison. It can also be used to generate,
in a somewhat restricted way, scripts for plays and motion pictures. This
could be done either a line at a time or by doing full-page makeup before
generating output.
        Next, we will devise a program that lets us peer into nontext files
that cause PR and DOS text-file programs (TYPE, for example) to produce
strange and not particularly useful output.



Chapter 10  Displaying Non-ASCII Text



        Our kit of programmer's utilities now contains some pretty useful
tools: the CAT program for concatenating files and PR to print them out on
paper; LS to list a directory in a variety of display formats; TOUCH to
assist the MAKE command in building programs; and RM to help us clean up
our disk directories easily. In addition, we have a raft of low-level and
mid-level routines to help us create other tools.
        Thus far, we have been concerned only with text files and tools that
permit us to view and manipulate them. However, one of the essential tools
that a programmer needs to have at the ready is a utility that permits the
inspection of nontext files. Nontext files are produced by compilers,
tokenizing interpreters (such as Microsoft interpretive BASIC), assemblers,
program configurators (see Chapter 7), and many commercial word
processors, database-management systems, and spreadsheets.
        If you were asked to convert a file produced by a word processor that
you do not have to a form that is acceptable to one that you do, how would
you go about it? Ignoring the possibility of buying the needed word
processor, we will have to do some detective work, which is what makes the
computer business such a lot of fun for some and such a pain in the neck
for others.
        The form and content of the data file would have to be determined so
that it could be converted to a suitable form for the target word
processor. A straight ASCII text file would suffice as data to be merged
into the input stream of most word processors, so we would first attempt to
remove any non-ASCII characters from the data file and save the results of
the conversion in an intermediate file for later processing.
        But how would we examine the data file? Our current tools are suitable
only for text files, so we need something else. In this chapter, we will
develop a program called DUMP that takes any file and produces an output
listing that looks like the dump output of the DOS DEBUG program.
        Each line of the output listing includes a hexadecimal offset in the
leftmost column, a hexadecimal display of 16 (10 hex) bytes from the data
file in the center, and the equivalent data in extended ASCII in the
rightmost column. A character code that would cause problems on the display
and on a printer is converted to a dot (.) before being displayed. These
filtered codes include newline, carriage return, and most other control
codes.
        The reason for developing a stand-alone program is that DUMP will
allow us to capture the converted output in a file or on paper for easy
examination. Our design will also permit DUMP to be used as a filter in
pipeline commands.
        Before designing and programming DUMP, let's review some of the most
common data-presentation formats and prepare some functions to do needed
conversions.


Some Useful Conversion Functions

        The primary data-presentation formats for numbers are the binary,
octal, decimal, and hexadecimal notations. While we humans take more or
less naturally to decimal (something to do with fingers and toes), our
machines tend to favor binary, in which things are on or off (1 or 0). The
octal (base 8) and hexadecimal (base 16) formats are simply notations that
make binary data more palatable to us. Octal number representations were
widely used for many years and still are in some quarters, but octal
notation has lost favor with the majority of programmers, who use
hexadecimal notation instead. All characters in the computer are
represented internally as binary numbers, however. That's all the computer
"understands." Our task is to receive data, convert it to the desired form,
and send it to the standard output. The output will be displayed on the
computer or terminal screen, or sent to some other device if stdout is
redirected.
        Our programs will need to convert the computer's internal
representation of numbers into decimal, hexadecimal, or character forms,
depending upon what we are trying to see. We will use two functions to do
this. They are used in DUMP and will be placed in the utility library
(util.lib) for use by other programs. The first function, byte2hex(),
converts a byte-sized (8-bit) quantity to hexadecimal. The second,
word2hex(), performs the same job on a word-sized (16-bit) quantity. These
sizes are appropriate for the IBM PC and other 16-bit machines, but they
are not universal, so portability to other machines is not guaranteed.
        Both byte2hex() and word2hex() are packaged in a single source file
named hex.c, which has the following contents:


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 275 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        The hextab array is a conversion lookup table. It uses a number in the
range of 0 through 15 as an index into the table, where the corresponding
character constant (number or letter) is found. Hence, the numeral 3 is
represented by a character constant of 3 (ASCII code 51--be sure you know
the difference), and the number 10 is represented by an A. These half-byte
quantities are comically referred to as "nibbles" because they're smaller
than bytes. The symbolic constant NIBBLE is defined as 0x000F. It is used
to mask off all but the four bits of interest in each conversion step.
Right-shift operations are used to pull the needed bits into the correct
position for conversion to a hexadecimal digit.
        A byte can be conveniently expressed as two hex characters (two
nibbles) that represent values in the range of 0 (00 hex) through 255 (FF
hex). The same range in binary is 00000000 through 11111111, which
obviously lacks notational convenience. The hex notation is more compact,
requiring only two characters to represent any value in the range.
        Now that we have a simple means of displaying data in hexadecimal, we
can move on to the development of the DUMP program.


A DEBUG-style File-dump Program

        The design of DUMP is strongly influenced by the expectation that its
output will be easy to feed to other programs and to a generic printer. We
want to build in enough flexibility to permit DUMP to be used as a filter
or as a stand-alone program. In stand-alone operation, DUMP takes a list of
files as arguments and produces the hex/ASCII output for each in sequence
on its standard output. As a filter, it receives a stream of arbitrary
characters and transforms it to the hex/ASCII output stream.
        The proposed format of DUMP's output looks a lot like that of DEBUG.
One difference is that the offset of each block of 16 bytes will be shown
relative to the start of the displayed file. We won't know where the file
is located in memory, so the offset value will stand alone. Another
difference is more subtle. DEBUG converts all non-ASCII characters in the
text display area to dots. DUMP will display all standard ASCII characters
plus the IBM extended ASCII character set when output is directed to the
screen. A command-line option will allow us to strip all nonprintable
characters from the output stream so that we can use any type of printer as
an output device.
        Figure 10-1 on the next page is the manual page for DUMP.
Note the overall simplicity of the program. Therein lies its power, because
it can be used in a wide range of situations without change.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the figure found on page  ║
                ║ 278 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝

        FIGURE 10-1 ▐ Manual page for DUMP


        The code for DUMP is straightforward. DUMP follows the pattern
established in Chapter 8, where file-oriented utility programs used
similar command-line options to control program operation and write to the
standard output channel. Here is the pseudocode description of the main()
function of DUMP, excluding the generic items, such as getting the program
name from DOS and error checking at each step:

            if (no files named)
                    hexdump(stdin)
            else
                    for each file
                            open(file)
                            hexdump(file)
                            close(file)

        The hexdump() function does the work of transforming the input into
the desired presentation format:

            for each block of input [BUFSIZ]
                    for each block of bytes [NBYTES]
                            copy byte offset into output buffer
                                    [word2hex()]
                            copy hex values into output buffer
                                    [byte2hex()]
                            copy ASCII values into output buffer
                            copy output buffer to stdout

        The main() function of DUMP uses getopt() to scan the command line for
optional arguments and filename arguments. If the -s option is found, the
Boolean variable sflag is set to TRUE to indicate that non-ASCII characters
should be stripped. If the -v option is found, the Boolean variable vflag
(verbose) is set to TRUE and a header consisting of a blank line followed
by the name of the file being dumped is output before the formatted data
for each file is listed. You may want to add other data to the verbose
output, such as file date and time, character count, and so on. If no
filenames are specified, DUMP reads from its standard input and the verbose
option is ignored because no name is associated with the data stream.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 279 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        One or more filenames can be specified either directly or by wildcard
specifications. Like CAT and PR, DUMP will operate sequentially on a list
of files in the order in which they are presented. Ambiguous filenames
under Microsoft C are expanded in lexical order, not directory order.
        Files are opened in binary mode, not translated mode, because we will
most likely be reading nontext files. Even if we read a text file, we want
to be able to see all characters, so the translation of the CR/LF and Ctrl-
Z codes is inappropriate.
        The hexdump() function uses an internal buffer, outbuf, to form a line
in memory before outputting it with the fputs() library function. Although
the buffering is not essential, it collects the writing operation into a
single function call instead of having five separate calls to several
different library routines. This buffering approach makes it easier to
change the output code if we decide to use a different means of writing the
output to the screen.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 282 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        Figure 10-2 on the next page is a sample of the output
produced by DUMP. The figure shows the hexadecimal and equivalent text
output produced by DUMP when it uses the hex.obj object file as
input. DUMP was not instructed to strip all nonprintable characters from
the output listing.


╓┌─────────┌────────────────────────────────────────────────┌────────────────►
00000000  80 07 00 05 68 65 78 2E 43 BE 88 07 00 00 00 4D  │Ç...hex.C╛ê....M│
00000010  53 20 43 6E 88 05 00 00 9F 45 4D 42 88 09 00 00  │S Cnê...ƒEMBê...│
00000020  9F 53 4C 49 42 46 50 10 88 08 00 00 9F 53 4C 49  │ƒSLIBFP.ê...ƒSLI│
00000030  42 43 64 88 07 00 00 9F 4C 49 42 48 B3 88 06 00  │BCdê...ƒLIBH│ê..│
00000040  00 9D 30 73 4F E3 88 06 00 00 A1 01 43 56 37 96  │.¥0sOπê...í.CV7û│
00000050  2E 00 00 06 44 47 52 4F 55 50 05 5F 54 45 58 54  │....DGROUP._TEXT│
00000060  04 43 4F 44 45 05 5F 44 41 54 41 04 44 41 54 41  │.CODE._DATA.DATA│
00000070  05 43 4F 4E 53 54 04 5F 42 53 53 03 42 53 53 3F  │.CONST._BSS.BSS?│
00000080  98 07 00 28 CA 00 03 04 01 67 98 07 00 48 10 00  │ÿ..(╩....gÿ..H..│
00000090  05 06 01 FD 98 07 00 48 00 00 07 07 01 0A 98 07  │...²ÿ..H......ÿ.│
000000A0  00 48 00 00 08 09 01 07 9A 08 00 02 FF 03 FF 04  │.H......Ü... . .│
000000B0  FF 02 56 9C 0D 00 00 03 01 02 02 01 03 04 40 01  │ .V£..........@.│
000000C0  45 01 C0 8C 2D 00 0A 5F 5F 61 63 72 74 75 73 65  │E.└î-..__acrtuse│
000000D0  64 00 09 5F 62 79 74 65 32 68 65 78 00 09 5F 77  │d.._byte2hex.._w│
000000E0  6F 72 64 32 68 65 78 00 08 5F 5F 63 68 6B 73 74  │ord2hex..__chkst│
000000E0  6F 72 64 32 68 65 78 00 08 5F 5F 63 68 6B 73 74  │ord2hex..__chkst│
000000F0  6B 00 A8 A0 14 00 02 00 00 30 31 32 33 34 35 36  │k.¿á.....0123456│
00000100  37 38 39 41 42 43 44 45 46 A8 A0 CE 00 01 00 00  │789ABCDEF¿á╬....│
00000110  55 8B EC B8 04 00 E8 00 00 56 8A 46 04 2A E4 89  │Uï∞╕..Φ..VèF.*Σë│
00000120  46 FE 8B 46 06 89 46 FC 8B D8 FF 46 FC 8B 76 FE  │F■ïF.ëFⁿï╪ Fⁿïv■│
00000130  B1 04 D3 EE 81 E6 0F 00 8A 84 00 00 88 07 8B 5E  │▒.╙εüµ..èä..ê.ï^│
00000140  FC FF 46 FC 8B 76 FE 81 E6 0F 00 8A 84 00 00 88  │ⁿ Fⁿïv■üµ..èä..ê│
00000150  07 8B 5E FC C6 07 00 8B 46 06 5E 8B E5 5D C3 55  │.ï^ⁿ╞..ïF.^ïσ].U│
00000160  8B EC B8 04 00 E8 00 00 56 8B 46 04 89 46 FE 8B  │ï∞╕..Φ..VïF.ëF■ï│
00000170  46 06 89 46 FC 8B D8 FF 46 FC 8B 76 FE B1 0C D3  │F.ëFⁿï╪ Fⁿïv■▒.╙│
00000180  EE 81 E6 0F 00 8A 84 00 00 88 07 8B 5E FC FF 46  │εüµ..èä..ê.ï^ⁿ F│

        FIGURE 10-2 ▐ Hex dump sample (HEX.DMP)


        When the -s option is selected, the ASCII text display is surrounded
by vertical lines formed from the vertical bar symbol (|). The normal
output, which assumes that the destination device is the PC screen, uses
the IBM drawing character (code 179) for a single vertical line; this looks
pretty on the screen, but cannot be printed correctly by many printers.
        The makefile for DUMP is contained in dump.mk:

    # makefile for dump utility

    LIB=c:\lib
    LLIB=c:\lib\local

    dump.obj:       dump.c
            msc $*;

    hexdump.obj:    hexdump.c
            msc $*;

    dump.exe:       dump.obj hexdump.obj $(LLIB)\util.lib
            link $* hexdump $(LIB)\ssetargv, $*, nul, $(LLIB)\util;

        To compile DUMP, first compile hex.c and add the object file to the
utility library (\lib\local\util.lib); then type

        MAKE DUMP.MK

The resulting executable file, DUMP.EXE, should be placed, as usual, in a
directory that is accessible to DOS via the PATH variable, so that it can
be called from anywhere in the directory hierarchy.
        The DUMP program can now be used for one of its intended purposes--
determining the file format of a special-purpose data file. This will help
us build another useful tool, SHOW, which lets us see nonprintable
characters in files in an unambiguous way and recover text from specially
formatted files.


The SHOW Program

        The next program is called SHOW because it shows us things that we
might not otherwise see. SHOW closely resembles DUMP in construction and
purpose, but its output is very different from DUMP's.
        If the task at hand is to retrieve the essential text from a document
file, rather than to determine how the formatting was done, we need a tool
that filters out the nonprintable codes and emits a stream of ASCII-only
text. In some situations, we might also want to see the formatting codes
represented visibly.
        The SHOW program takes files that may contain special (usually
nonprintable) characters in addition to normal text and produces an ASCII
text output. Nonprintable codes are converted to their displayable
hexadecimal notation--a backslash followed by two hex digits.
        Two options allow us to strip out special codes and produce an output
that is pure text. The -s option strips all special codes except the usual
format effectors (newline, tab, and space).
        The -w (word-processor) option strips special codes (by setting the -s
option), but it handles several additional elements of word-processor data
files as well. For example, when forming paragraphs, many word processors
use a carriage return as a "soft" return (line break) and a combined
carriage return and linefeed as a "hard" return (paragraph terminator). For
compatibility with such word-processor file formats, the -w option of SHOW
converts a lone carriage return to a newline, thus breaking long lines into
a sequence of shorter ones. Also, some word processors use the eighth bit
of some bytes to signal formatting options (bold, underline, end-of-word,
and so on). To make these bytes visible, SHOW (with the -w option set)
turns off the high bit of all bytes and displays the bytes that then fall
into the printable ASCII range.
        The full manual page for SHOW is presented in Figure 10-3.
Notice that SHOW is also a filter: It can read from its standard input
channel and write the transformed output to the standard output
channel.
        The pseudocode description for SHOW is nearly identical to that of
DUMP. The primary differences are found in the showit() function, which is
described (without options) by the following pseudeocode:

            for each input character
                    if it's ASCII
                            if it's printable or a format effector
                                    send it to stdout
                            else
                                    send printable equivalent to stdout

        The options -s and -w add some minor complications to the description.
The effects of the options are best seen in the code for the main() and
showit() functions. In main() (in the file show.c), getopt() is once again
used to process command-line options. A variable, wflag, controls whether
SHOW attempts to convert word-processing data files. It is initially FALSE.
The call to getopt() requires the addition of w to the allowed option list.
The value of wflag is passed to showit() as an argument, rather than being
treated as global data. If SHOW had a lot of functions that needed to know
about wflag, we would probably make it a global variable.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the figure found on page  ║
                ║ 287 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝

        FIGURE 10-3 ▐ Manual page for SHOW


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 287 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        The showit() function receives a FILE pointer and the values of sflag
and wflag. If neither of the flags is TRUE, showit() passes anything that
is printable (including format effectors) to the standard output channel
and converts everything else to a displayable hexadecimal byte
representation. Thus, the null byte is shown as \00, the ASCII Del
character is shown as \7F, and so on. If -s was used on the command line,
sflag (and therefore strip) is TRUE, which causes showit() to eliminate the
hexadecimal conversions.
        Setting the -w option sets the wp variable in showit() to TRUE,
causing the function to alter the default treatment of carriage returns and
extended characters.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 290 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        The makefile for SHOW is similar to the one for DUMP:

    # makefile for the show program

    LIB=c:\lib
    LLIB=c:\lib\local

    show.obj:       show.c
            msc $*;

    showit.obj:     showit.c
            msc $*;

    show.exe:       show.obj showit.obj $(LLIB)\util.lib
            link $* showit $(LIB)\ssetargv, $*, nul, $(LLIB)\util;

        To see the output of SHOW, try the program on a data file from a word
processor or spreadsheet with and without the optional processing features.
The results should be at least intriguing and will probably be enlightening
to those who have not snooped around in such files before.
        In the next section, we move from the line-by-line realm of
file oriented programs to the intensely visually oriented programs that
make the PC so popular with the average user.



SECTION IV  Screen-Oriented Programs



Chapter 11  Screen Access Routines



        In this chapter and the next two, we will explore some of the many
options available to designers for presenting information on a PC screen.
The method of screen access presented in this chapter is relatively easy to
implement, produces excellent results, and costs little in terms of program
size and complexity. Its use is not recommended for programs that will
operate in a windowing environment, however, because it violates one of the
primary rules of good behavior: "Thou shalt not write directly to the
screen."
        We will purposely break this rule at first to show what level of
performance we are striving for. In Chapter 12, we will explore some
interesting display buffering techniques and show how we can be "good" boys
and girls by using BIOS-based screen routines as an alternative to direct
access. Then, in Chapter 13, we will seek to be downright virtuous by
using the ANSI standard interface that is portable to nearly every PC that
runs a version of MS-DOS (and PC-DOS).


Determining the Display System Type

        One of the challenges that has always faced PC software designers is
writing a program that determines what type of display adapter and monitor
is in use before it does anything else. It is unfortunate that many
programs ignore this crucial first step.
        Many of the offending programs were designed by programmers working on
monochrome-only systems. When run on a color/graphics-equipped system,
these programs at least alter the user's selected video attributes so that
either during or following the program's running, the screen is unpleasant
to look at or has a whole new shade of reality. Sometimes, but not too
often, the screen gets completely messed up by being left in an
inappropriate video mode.
        Programs designed for color-only systems can produce similarly
unpleasant effects on monochrome systems. For example, light blue is a very
pleasing color for text, but it causes everything on a monochrome system to
be underlined and intense. Then there's the problem of a program that
assumes the needed display adapter is in use and does not check for its
presence. This can leave the user with a system that appears to be
frozen.
        And it isn't just amateur programmers who are guilty of these
transgressions. Although most commercial software offerings check the
hardware, many supposedly "commercial quality" programs have the bad
manners to ignore the user's color preferences, video mode settings, and so
on.
        We can do something about this situation. Following are some routines
and program examples that show how to detect, save, and restore the user's
operating conditions. The routines use BIOS functions and simple memory
tests to determine what equipment is installed. You can call the functions,
as demonstrated in the DSPYTYPE program that follows and the ScreenTest
(ST) program later in this chapter, and save the video state at the time
your program begins execution. The original operating conditions can then
be restored before the program returns control to DOS.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 296 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        The DSPYTYPE program is a test driver for the functions memchk() and
ega_info(). These functions supplement the BIOS equipchk() and getstate()
functions we saw in Chapter 5. The memchk() function looks for memory at a
specified segment and offset. It is a small-model function that calls on
the library function movedata() to write and read the contents of memory
locations. A large-model version of memchk() would use memcpy() in place of
movedata(). The memcpy() function takes long pointers used in large-model
programs; movedata() requires segmented addresses to access far data items
in small-model programs.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 298 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        The technique used to detect the presence of a monochrome display
adapter (MDA) or a color/graphics adapter (CGA) involves a sequence of
operations that save, write, read, compare, and restore the contents of a
memory location that is known to be in the range of the given adapter. The
save and restore operations ensure that the test is non-destructive. The
write-read-compare operation determines whether there is any active memory
at the test location. The value 55H is a bit pattern of alternating 0s and
1s. Because unoccupied memory locations tend to look like 1s to programs
that try to read them, the pattern of the test value used  by memchk() is
not likely to be duplicated. If you want to feel more sure of the results,
use sequential tests at the same address with two or more different
values.
        But why bother looking at the memory--why not simply read the mode
value returned by getstate()? We will probably want to do both in our
programs. Use getstate() to find out what the operating video mode is, and
look for memory at the other adapter locations. Some PC users (programmers
in particular) have both monochrome and color display systems installed in
a PC. A program that was designed to work exclusively with a CGA system
might find the system in mode 7 (monochrome). The program can respond to
this in two ways: It can print a message stating its need for a CGA and
quit, or it can look for a CGA system and switch to it if one is found. I
favor the first approach because the user may have the color system turned
off. He or she can use the DOS MODE command to select the needed display
mode (MODE MONO to select monochrome; MODE CO80, MODE CO40, and so on, to
select a CGA mode).
        The memchk() function has other uses, too, such as looking around for
blocks of memory that might be separated from the main memory of a system.
It cannot be used to detect the presence of read-only memory (ROM), of
course, because ROM cannot be written to and it will fail our tests. You
can use memchk() to check memory locations at one-kilobyte increments to
count up how much RAM is available to DOS and how much might be
noncontiguous. Many add-in memory boards permit splitmemory addressing,
allowing you to set up stand-alone print spoolers and other special memory
allocations. With memchk(), we have a convenient way to find them. Add
memchk() to the utility library to make it readily accessible to your
programs.
        The enhanced graphics adapter (EGA) complicates the video-adapter
detection problem a bit. The address space of the EGA nominally starts at
segment A000. However, an EGA's addressing modes are very flexible, and the
adapter usually masquerades as a CGA or MDA while operating at the DOS
command level, setting itself up to start at either the B800 or B000 memory
segments. The problem is in determining whether an EGA or some other
adapter is installed at a particular location. The function ega_info(),
based on information provided by IBM, looks for an EGA by calling the BIOS
video interrupt 10H and invoking the Alternate Function Select function
AH = 18 (12H), which is an EGA BIOS feature (the EGA BIOS is located at
segment C000H). When AL equals 10H upon entry, this function returns EGA
information, which includes the EGA mode (color or monochrome) and memory
size (64 to 256 KB) in addition to feature and switch-setting information.
An EGA display system is absent if ega_info() returns mode values other
than 0 and 1 and memory size values outside the range of 0 to 3.
        If an EGA is found, checking the video mode returned by getstate()
tells us whether it is emulating a CGA or MDA. If we need to know if
another adapter is installed, we can use memchk() to do the test. If an EGA
is emulating a CGA, you cannot have a real CGA installed, so we need check
only for the adapter type not being emulated by the EGA.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 301 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        DSPYTYPE is a simple program that calls ega_info() and memchk() to
determine what display adapters are installed in a system and to report the
results to the user. DSPYTYPE also reports the current video mode, screen
width, and screen page number. In practice, we would use the functions to
get the video mode and the display adapter configuration, and then set up
the needed display conditions for our program or abort with an explanatory
message about the lack of needed hardware.
        Next we will consider a method of direct screen access for reading and
writing that serves as an alternative to using BIOS and DOS methods of
screen access. The BIOS and DOS functions automatically deal with the wide
variety of display configurations that are possible in a PC system, but
they introduce a lot of processing overhead because an adapter test occurs
with every character (or dot) that is written or read. The display-adapter
detection methods just described are important to direct screen access
because they let us do the adapter tests once at program start-up, cutting
out a huge amount of processing during subsequent screen read and write
operations.


Direct Screen Access

        A fast but inherently nonportable way of updating the PC screen is to
write directly to its associated memory. Using one or more off-screen
buffers in the program's data space to create images before displaying them
can produce excellent visual performances. A block-copy routine quickly
copies the buffered-memory image to physical display memory, producing an
"instant screen" effect. The IBM monochrome display adapter exhibits no
problems with this strategy. Neither do the IBM enhanced graphics adapter
and many third party monochrome and color/graphics adapters, all of which
have been designed to permit simultaneous access to display memory by both
the CPU and the display-refresh circuitry.
        The original CGA in either 40-column or 80-column text mode poses a
problem for designers because, unlike the monochrome adapter, the CGA
exhibits visible interference when a program tries to access display memory
while the monitor screen is being updated (refreshed) from the same memory.
The interference looks like "snow"--an irritating pulsing of short line
segments covering all or portions of the screen. Several methods of
avoiding the interference have been developed. One is to synchronize the
display accesses during both reading and writing operations with the time
periods within a display-refresh cycle that are considered "safe." The safe
times are the horizontal and vertical retrace periods of each displayed
frame. Another method involves blanking (turning off) the raster scan while
the display memory is being written to. As we'll see, each approach has
advantages and disadvantages.


        Display Adapter Basics

        We begin with a brief examination of the color/graphics adapter to see
why the retrace periods are the only safe times for display memory
accesses. This discussion is not applicable to the IBM enhanced graphics
adapter because it uses faster memory devices and additional logic that
prevent problems that occur when the CPU and video-refresh circuitry try to
access the video buffer simultaneously.
        The memory on the standard CGA is placed within the address space of
the central processor. The CGA memory starts at segment B800H and extends
upward for 16 KB, enough memory for one high-resolution graphics screen
(128,000 picture elements), or four screen pages in 80-column color text
mode. The text mode is the focus of this discussion.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║    Figure 11-1 is found on page 303      ║
                ║    in the printed version of the book.   ║
                ║                                          ║
                ╚══════════════════════════════════════════╝

        FIGURE 11-1 ▐ Typical horizontal sweep signal


        Figure 11-1 on the previous page represents the horizontal
sweep signal, which is the signal within the display device that is
responsible for the horizontal deflection of the electron beam that paints
the screen. The figure depicts one horizontal scan period. The dependent
(vertical) axis depicts the amount of beam deflection as a function of time
shown along the independent (horizontal) axis. On a computer display
device, the image on the screen is under-scanned so that the image is
completely visible within the normal viewing area, resulting in a framed
picture with a visible border. Television sets, on the other hand, use
over-scanning to make the image "bleed," leaving no border. The beam is
turned off completely (blanked) during retrace to avoid leaving unwanted
residue on the screen.
        The IBM display is not interlaced, so there are 262.5 horizontal scans
per frame (one full screen image), and frames occur at a rate of 60 per
second. With 15,750 horizontal scans per second, each one takes about 63.4
microseconds. Only a small portion of a single scan, typically 20 percent
or less, is allocated to the horizontal retrace--one of the safe times for
display memory accesses, as can be seen in the figure.
        The horizontal sweep signal moves the electron beam from side to side,
and if it were the only sweep signal affecting the beam, there would be
only a single straight line on the display surface. Another kind of sweep
signal is needed to move the beam up and down the face of the tube. The
vertical sweep has the same basic sawtooth shape as the horizontal sweep,
but a slower rate of change. At minimum deflection, the beam is at the top
of the screen; it moves toward the bottom with increasing deflection.
        There is a vertical retrace period at the end of each frame that
occurs during the vertical-sync pulse period. During this time, the
electron beam is blanked and moved from the lower right corner of the
screen back to the upper left corner. The vertical-sync period (typically a
little more than one millisecond for the standard American TV signals used
by the CGA) is long enough to permit a block of about 250 data words (2-
byte character and attribute pairs) to be transferred to or from display
memory without interference. Most video controller designs disable the
horizontal sweep during the vertical retrace, but some let it run
continuously.


        Programming Considerations

        A few important choices affect the way programs that interact tightly
with the display system are designed and written. There are enough choices
at every step in the process of designing a video application so that no
two designers are likely to do the job in exactly the same way. The
following program implements one way of designing a video interface. It is
not the only way. Other methods that are even more intimately tied to
specific hardware can run as much as four times faster than the method
described here, but they are much less portable to other hardware
configurations.
        I decided to use a buffered screen interface. This means that the
image to be sent to the display is assembled in the program's own data
space. When complete, the image is copied in its entirety as quickly as
possible to display memory.
        Conversely, an unbuffered approach is used by many programs and is
adequate for most purposes. In the typical unbuffered scheme, characters
are written into display memory via DOS and BIOS routines, but no memory
image is retained by the application program. The DOS and BIOS routines can
also be used in a buffered screen-management system but will slow things
down quite a bit compared to direct methods. As we'll see in the next
chapter, careful design can greatly improve the apparent speed of the DOS
and BIOS routines under many operating conditions.
        The routines described in this chapter assume that programs calling
them have already done an equipment inventory and have set up the display
system in an 80-column text mode. The getstate() function obtains the
current video mode, screen width, and visual display page values. Mode
values of 2 or 3 indicate 80-column text modes (monochrome and color,
respectively) on a CGA, and a value of 7 indicates a monochrome adapter.
The DOS MODE command may be used to select an appropriate video mode before
calling programs that require a particular mode.


        Alternative Solutions

        Available methods of synchronization to avoid visual interference on a
CGA depend on the use of the status register at I/O address 3DA hex. This
is a read-only register on the CGA that has two bits of interest to the
block-copy routine described below. When high (1), bit 0 indicates that a
horizontal retrace is in progress. When high, bit 3 indicates that a
vertical retrace is in progress.
        Another register--a write-only register at I/O address 3D8 hex on the
CGA--has a bit that may be reset (made 0) to disable video. Bit 3 must be
set to a value of 1 to turn on the beam that paints the screen. Turning off
the beam is an effective way to prevent visual interference from reaching
the viewer's eye. However, the beam cannot be left off for more than about
three character rows' worth of data before a flicker becomes apparent. The
BIOS video scroll routines use this technique and are not pleasant to look
at if the background color is anything but black. Even normal text on a
black background appears to dim somewhat in the upper half on the display
when the blanking method is used.


A Synchronized Block-copy Routine

        The direct method of screen access uses a memory buffer that holds the
same amount of data as one display page on the standard CGA. The block-copy
routine, cpblk(), copies the contents of the memory buffer to display
memory only during safe times. The memory buffer has a total of 4000 bytes;
2000 bytes are for characters (25 rows by 80 columns), and the other 2000
bytes hold the attributes associated with the characters. Display memory
has 4096 bytes per page (four pages in 80-column mode), but the last 96
bytes of each page are unused (except by some programs that hide
information in them).
        The C-language source for the block-copy routine is cpblk.c. An image
is copied from application memory to display memory as a series of ten
blocks of 200 words each. Each word represents a character and its
associated video attribute. (An assembly-language version of this routine
is described in my "Instant Screens" article in the June 1986 issue of
PC Tech Journal. Portions of this chapter are based on that article.)


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 306 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        The C version of cpblk() is about 60 percent slower than its assembly-
language counterpart, but it is fast enough for most purposes, copying an
entire screen in about two-tenths of a second. The assembly-language
version uses both horizontal and vertical retrace periods to reduce the
number of blocks needed to six instead of ten. The use of ten blocks is
conservative and will work with nearly all IBM-compatible  display reduce
the number of blocks needed to six instead of ten. The use of ten blocks is
conservative and will work with nearly all IBM-compatible display hardware.
On all of the IBM machines I tested, eight blocks were sufficient; however,
some compatibles required smaller blocks because of differences in the
timing of the retrace signal used to determine the safe time for copying
data.


Double Buffering

        To obtain the snappiest looking performance from a buffered screen-
interface technique, programs can use an in-memory screen buffer that is
updated out of view of the user and then is copied to display memory as
fast as possible. A way of achieving nearly instant CGA updates is shown in
Figure 11-2. The technique is called double buffering because two levels
of buffers are maintained in the application program and elsewhere in main
memory. A two-step process is used to form a composite image in a screen
buffer before it is copied to physical display memory.
        Data source buffers may be of any size and are usually thought of as
being rectangular in shape although they are simply sequences of bytes in
memory. They are mapped to the off-screen buffer as needed--a technique
that permits windows for help frames, menus, and the like to be easily
overlaid onto another image. In the next chapter, we will write a set of
library functions that handle the needed operations, such as writing
characters, attributes, and strings, scrolling regions, and so on, and
demonstrate the technique in a sample program.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║    Figure 11-2 is found on page 308      ║
                ║    in the printed version of the book.   ║
                ║                                          ║
                ╚══════════════════════════════════════════╝

        FIGURE 11-2 ▐ Double buffering


        Once the off-screen buffer has all of its characters and attributes in
the right places, the task of getting the data to the visual display is
handled by the cpblk() routine described above. If the screen buffer is
copied directly to the part of display memory being viewed, the user will
see the screen being updated, albeit very quickly. If the contents of the
before and after images vary only in small areas, it is difficult to notice
that an update occurs.
        If, however, there are massive image changes, such as switching
background colors, the user will detect the update. For some purposes, the
visible updating of screen displays is desirable because it reassures the
user that the program is doing something. In other situations, we will want
screen images to snap instantly into place. We can have it either way.


Demonstration Program

        The file st.c contains the C-language source for a test driver program
called ST (for ScreenTest). The driver program uses a static screen buffer
to hold text images that are copied en masse to display memory after the
image is fully constructed.
        The getkey() function (see Chapter 5) calls DOS function 7 hex, which
returns the next available character in the keyboard buffer. The function
waits for a keypress if nothing is ready. The driver program displays a
help message for any nonprintable keypress except Esc (the Quit command)
and Ctrl-Break (Abort). As each character is keyed in, its value is used to
fill the screen buffer. Differing color attributes derived from the
character code are used to show the effects of massive color and intensity
changes. The attribute for a given character is the code for the character
shifted left 8 bit positions. The high bit is masked off to prevent screen
blinking.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 309 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        The driver program uses a trick called "page flipping" to produce the
appearance of instant screen displays. It actually takes a few tenths of a
second to copy a screen buffer in the application data space to the display
adapter using the cpblk() routine. Although this is fast when compared to
all other methods that avoid video interference, it is still far from
instantaneous. The page flipping method is possible because the CGA has
enough display memory to hold multiple screen pages simultaneously.
        An illustration of page flipping is presented in Figure 11-3.
The method depends on having a means of telling the display system to
view one page of display memory while the application program is writing to
another. The ROM BIOS video interrupt includes a function (5) that sets the
visual page.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║    Figure 11-3 is found on page 313      ║
                ║    in the printed version of the book.   ║
                ║                                          ║
                ╚══════════════════════════════════════════╝

        FIGURE 11-3 ▐ Page flipping


                                COMMENT
            Ignore the Technical Reference statement that the
            function sets the "active" page. To be consistent with
            the way BASIC describes video pages, the active page
            should be the one being written and the visual page
            should be the one being viewed. Most frequently, the
            visual and active pages are one and the same display
            page.

        Notice that the fprintf() standard library function writes to standard
error, which appears on the visual page. The cpblk() routine, however, is
directed to write to the active page, which is effectively hidden from
view. When the active page has been fully written, it is revealed to the
user by flipping the pages. The function swap_int() exchanges the values of
the apg and vpg variables. The source for swap_int() is contained in the
file swap_int.c. The function is useful in other contexts, so we will add
it to our utility library.

    /*
    *      swap_int -- exchange the values of the two integers
    */

    void
    swap_int(p1, p2)
    register int *p1;
    register int *p2;
    {
            int tmp;

            /* exchange the values */
            tmp = *p1;
            *p1 = *p2;
            *p2 = tmp;
    }

        After building the active page, ST then calls setpage(), another of
our bios library routines from Chapter 5, to switch to the new visual
page. The effect from the computer user's perspective is that of an
instant update. There is a short delay while the active page is being
updated before the page swap, but it's not noticeable to the user. The
screen is repainted in one-sixtieth of a second, far faster than the human
eye can follow, and the response to a keypress is usually completed before
the key is released.
        The special efforts to synchronize with the vertical-retrace signal
taken by the cpblk() routine are not needed when an MDA is being used.
Therefore, the driver program, ST, checks for mode 7 and uses a standard
block-copy routine that invokes the string-copy feature of the 8086/88
processor. A string copy of 4 KB is done very quickly. There is no visible
flicker and no apparent delay when this approach is used.
        An option on the invocation command line for ST permits a single
argument (anything will do) to be given to turn off the special copy
feature when operating on an EGA or on a compatible computer that does not
experience display interference. Using the command ST x, for example, will
tell the program to use a standard block-copy routine instead of the
special one. If this option is selected on a system with a standard CGA,
visible interference will be quite noticeable, especially if you hold down
a key to repeatedly write the same data to the display.
        The makefile for ST, st.mk, contains the instructions needed by MAKE
to build and maintain the ST programs.

    # makefile for ScreenTest (ST) program

    LLIB=c:\lib\local

    swap_int.obj:   swap_int.c

    cpblk.obj:      cpblk.c

    st.obj:         st.c

    st.exe:         st.obj swap_int.obj cpblk.obj $(LLIB)\bios.lib
                    $(LLIB)\dos.lib
            link $* swap_int cpblk, $*, nul, $(LLIB)\bios $(LLIB)\dos;


Design Considerations

        Because it takes a few tenths of a second to copy data from a screen
buffer to display memory, programs should not attempt to write one
character at a time from the keyboard. This would result in a maximum
update rate of a few characters per second. Even a very slow typist would
get way ahead of such a program.
        A better way to handle this situation is to use a modified cpblk()
function that updates only the changed character or the rectangular region
in which changes have been made to the buffer. We could also use routines
based on the BIOS and DOS interrupts to update the visual display page
(they do so without causing interference) and use a separate routine to
update the in-memory buffer so that it continues to track what is being
displayed.
        The screen-update routines mentioned above can be fashioned from the
cpblk() function. For example, a routine that copies a single line or a
small range of lines, or one that copies a small rectangular region from a
screen buffer to display memory, would be useful in doing selective screen
updates with shorter delays. The next chapter describes screen-buffer
routines that do these and other screen-buffer management tasks.
        Remember that the cpblk() routine described here is inherently less
portable than the DOS and BIOS calls and thus should be used only when
necessary for speed and effect. The symbolic constant that holds the
display memory segment could be replaced by a variable. It would then be a
simple matter to change the value for machines that put display memory in a
nonstandard place, if a reliable method can be developed for determining
the identity of the host hardware and its display memory location(s). There
is no foolproof method of doing the hardware identity tests; the best we
can do is look for copyright notices and company names, but finding out the
exact machine type remains an elusive goal. In my programs, I make the
assumption that displays are in the standard IBM-specified locations. This
works on 90 percent of the machines currently in use. For the other 10
percent I produce customized versions of the programs and charge a little
extra to compensate for the added effort.



Chapter 12  Buffered Screen-Interface Functions



        The purpose of this chapter is to extend the screen interface
presented in Chapter 11 by building a set of routines that let us
directly access a screen buffer as a virtual screen.


A Buffered Screen-Interface Package

        We have already seen how a memory image of a text screen can be
quickly copied to physical display memory, giving the appearance of instant
or at least fast screen updates. Our concern now is how to form the memory
image of the desired display. We want to form the images in a way that
simplifies the effort needed to manage several distinct areas of the
screen. For example, a screen may be divided into viewing areas devoted to
user commands, program status, and text. A provision might also be made to
display help frames in a region that partially or completely overlays
another area of the screen.
        While developing the interface presented in this chapter, I wrote a
test driver program (SB_TEST) that exercises each interface function to
assure correct action. The test program creates several screen regions (or
windows) that can be filled, cleared, scrolled, and so on, under control of
the user at the keyboard. We'll develop the functions first, then apply the
driver to show off some of the capabilities of the screen interface.
        An element in display memory consists of two bytes: one representing a
character and the other an associated attribute. This design is
considerably more flexible than the design used by many low-cost terminals
that use a single byte for each screen location. The latter design requires
that some screen positions be "wasted" (they appear blank) because they are
used to select attributes for all positions that follow until the next
attribute-setting position. (The attribute positions are referred to by
some as "magic cookies.") The PC design requires twice as much storage but
permits attributes to be assigned on a character-by-character basis.
        We will form a virtual-screen buffer as a two-dimensional array of
"cells," with each cell being represented as a union. This allows our
programs to access each cell as individual character and attribute bytes or
as a single character/attribute pair. The definitions of a cell, the
virtual-screen buffer, and other components of the buffered screen
interface are contained in sbuf.h.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 318 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        Buffer Management Functions

        One of the other components referred to above is a definition of a
REGION, a structure that effectively defines a window, which in this
implementation is simply a rectangular region of the screen buffer. To keep
things reasonably simple, this interface keeps all screen data (characters
and attributes) in the screen-buffer array, which is created by sb_init().
There are no "shadow" buffers for individual windows.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 320 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        The screen buffer is allocated as global data in sb_init.c. It has
type union CELL. The declaration

        union CELL Scrnbuf[SB_ROWS][SB_COLS];

uses the constants defined in sbuf.h to allocate 4000 bytes of storage for
the screen buffer. The sb_init.c file also allocates the Sbuf structure
that holds screen-buffer control information. Sbuf is of type struct
BUFFER. It contains "cursor" row and column position variables, a pointer
to the screen-buffer array, 2 bytes (unsigned short) of status flags, and
arrays of column positions that mark the beginning and end of changes to
each screen-buffer row. The range limits allow the routine that copies the
screen buffer to physical-display memory to restrict the number of
characters that must actually be written, thus minimizing the time needed
to do display updates.
        The sb_show() function (in sb_show.c) is capable of using several
methods to copy the screen-buffer contents to display memory. The buffer-
copying method that is actually selected depends upon the user-specified
access type (UPDATEMODE equal to DIRECT or BIOS access) and the type of
display system that is installed in the host machine (IBM monochrome
display adapter, or MDA, versus all other types).


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 321 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        The sb_show() function in DIRECT update mode checks to see how many
screen-buffer rows differ from those currently displayed. If there are two
or fewer, they are copied individually. Any greater number of changed
screen rows causes a block-copy operation (based on the cpblk() function
that is described in Chapter 11) to be performed. If the DOS variable
UPDATEMODE is not set, BIOS is assumed. Adding the line

        set updatemode=direct

to one's AUTOEXEC.BAT file (or typing it on the DOS command line) causes
screen updates to use the faster, but less portable, direct-access
method.


        General Window Functions

        Now that we can create a screen buffer and copy it to display memory,
we can proceed to prepare functions that place the data to be displayed
into the buffer. The user sees nothing until the image is fully formed and
copied. This allows programs to handle overlapping windows and special
effects without distracting the user. By writing the least amount of data
to the screen, this buffered-screen interface also speeds up the apparent
performance of programs. The following functions do the work.

        ►  sb_new(). As noted earlier in this chapter, screens are easier
        to manage if they are divided into separate regions where
        each region is devoted to a single purpose. The function
        sb_new() is used to describe a new window by allocating a window-
        control structure that contains row and column values for two
        opposite corners (upper left and lower right) of the window and a
        scrolling region within it, a "cursor" position, and a status flag
        word. Initially, the scrolling region is set to the same dimensions
        as the window itself. It can be set to other dimensions by calling
        sb_set_scrl().
                Space for the control structure is allocated by a call to
        malloc(). NULL is returned if there is not enough memory
        satisfy the request. A successful request results in a pointer to
        the window-control structure (struct REGION *) being returned. All
        positioning, character and string operations, scrolling, and
        clearing is done with respect to this window pointer.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 324 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        ►  sb_move(). This function is used to move the cursor to a specified
        location within a window. Unreasonable requests (outside window
        boundaries) elicit an error return code (SB_ERR). If the request is
        in bounds, both the window-relative and buffer-relative cursor row
        and column values are updated and SB_OK is returned.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 325 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        ►  sb_fill(). A window may be filled with a given character and
        attribute pair by calling sb_fill(). Every cell in the specified
        window is set equal to the character and attribute values passed as
        arguments. Two variations on the theme, sb_filla() and sb_fillc(),
        are used to fill a window with either an attribute or a character
        while leaving the other component of each cell undisturbed.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 326 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        ►  sb_putc(). This function puts a character into a window at the
        current cursor position and advances the cursor. Accommodation is
        made for wrapping at the end of a window row, and scrolling is
        forced when a character is written to the last position of the last
        row of a window. Scrolling can be disabled by clearing the
        SB_SCROLL bit in the window flag word (the default state). Standard
        format-effector control codes (newline, carriage return, linefeed,
        and tab) are treated normally.

        ►  sb_puts(). A string may be added to a window using sb_puts(), which
        simply calls sb_putc() to do its work for each character in the
        string.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 328 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        ►  sb_rca(). Used to read the character and attribute values from the
        screen-buffer cell at the current window cursor location. The
        functions sb_ra() and sb_rc() read the individual attribute and
        character values, respectively. This is a simple task because all
        the data is available in the screen buffer. In the case of
        sb_rca(), an unsigned short quantity is returned. The sb_ra() and
        sb_rc() functions return an unsigned char value.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 331 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        ►  sb_scrl(). To scroll a window vertically, use sb_scrl(). This
        function scrolls a region within a window. The scrolling region is
        frequently made smaller than the full window size to allow for a
        boxed border and a title line and other such trimmings. A
        comparable effect can be obtained by defining a window within a
        window, but then the regions must be managed individually, adding
        to program overhead.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 332 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        ►  sb_wca(). The sb_wca() function writes both a character and an
        attribute to a window at the current cursor position. A repetition
        count tells the function the extent of the write operation. The
        cursor position is not changed. The sb_wa() and sb_wc() functions
        write an attribute and a character, respectively, to a window. Note
        that sb_wca() combines the two unsigned char values into a single
        word by shifting the attribute left eight positions and bitwise
        ORing the result with the character value. We could achieve the
        same result by using separate assignment statements for the
        attribute and character components of the screen-buffer cell. The
        "write" routines are useful for such tasks as displaying horizontal
        portions of character graphics and changing the highlighting of
        text strings like those used in menu bars and pop-up selection
        menus.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 335 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        Box-drawing Functions

        A portion of the screen that demands special attention is often
highlighted by drawing a box around it. Some screen designs require that
particular areas of the screen be set off by box designs that signify their
purposes. For example, a program that displays multiple windows might
identify inactive windows by surrounding them with single-line boxes while
highlighting the currently active window with a double-line box.
        The IBM extended-ASCII character set (codes 128 through 255), which is
supported by nearly all PC-compatible computers, includes a group of line-
drawing characters that are suitable for drawing boxes and other shapes
with various combinations of single and double lines, full and partial
blocks, and various regular dot patterns.
        The sb_box() function can be called to draw a box around the perimeter
of a window. The box is drawn entirely within the bounds of the specified
window; therefore, to avoid overwriting it, text functions should be
instructed to write to an area at least one character within the box. Box
corners and edges are formed from characters that match correctly at all
junctions. The box types are defined in the box.h header file.

    /*
    *      box.h -- header for box-drawing functions
    */

    typedef struct box_st {
            short ul, ur, ll, lr;   /* corners */
            short tbar, bbar;       /* horizontal bars */
            short lbar, rbar;       /* vertical bars */
    } BOXTYPE;

    /* box types */
    #define BOXASCII        0
    #define BOX11           1
    #define BOX22           2
    #define BOX12           3
    #define BOX21           4
    #define BOXBLK          5


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 338 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


The Screen-Buffer Library

        The sbuf.lib is a library that contains the object modules for each of
the screen-buffer functions just described. The library has wide
applicability to screen-oriented programs. It will reside in the \lib\local
subdirectory for easy access by our programs. The makefile, sbuf.mk, using
the inference rules in tools.ini, keeps all objects and the library file
current and puts files in the required places. If you use a different
directory scheme, you should modify the makefile to accommodate the
differences.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 340 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


    # inference rules for SB_TEST program
    # (small model with function prototyping enabled)

    [make]
    .c.obj:
            msc -DLINIT_ARGS $*;


        A Test Driver Program

        The SB_TEST program (sources are in files sb_test.c and sb_test.h)
alluded to earlier is one of several programs I used to test the SBUF
library functions as they were being developed. The makefile (sb_test.mk)
presumes that the screen-buffer function library is up to date. To use the
test driver program, simply type its name at the DOS prompt.
        SB_TEST creates several windows and places some text in each. On a
color display, each window sports its own color scheme. On a monochrome
display, the status window displays in reverse video and the help window
contains bold text; the other windows display normal text.

    /*
    *      sb_test.h -- header for sb_test program
    */

    /* dimensions of the display windows */
    #define CMND_ROW        0
    #define CMND_COL        0
    #define CMND_HT         1
    #define CMND_WID        SB_COLS
    #define STAT_ROW        CMND_HT
    #define STAT_COL        0
    #define STAT_HT         1
    #define STAT_WID        SB_COLS
    #define TEXT_ROW        (CMND_HT + STAT_HT)
    #define TEXT_COL        0
    #define TEXT_HT         (SB_ROWS - TEXT_ROW)
    #define TEXT_WID        SB_COLS
    #define HELP_ROW        5
    #define HELP_COL        5
    #define HELP_HT         18
    #define HELP_WID        70


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 342 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


    # makefile for SB_TEST driver program

    LLIB = c:\lib\local
    LINC = c:\include\local

    sb_test.obj: sb_test.c $(LINC)\sbuf.h sb_test.h

    sb_test.exe: sb_test.obj $(LLIB)\sbuf.lib $(LLIB)\bios.lib
                    $(LLIB)\dos.lib
            link $*, $*, nul, $(LLIB)\sbuf $(LLIB)\bios $(LLIB)\dos;

        After compiling and linking the program, try using scrolling and
clearing commands in the help and text windows to see how the command
actions are implemented. Of course, a real program would display real text
rather than the repeated sequences of letters that are produced by the test
driver.
        The commands are described in the following table:

═══════════════════════════════════════════════════════════════════════════
    COMMAND                     DESCRIPTION
───────────────────────────────────────────────────────────────────────────
    Up and down arrows       │  Scroll up and down by one row
    PgUp, PgDn               │  Scroll up and down by one page (a windowful
                            │  minus one row)
    Alt-c                    │  Clear the current window
    Alt-h                    │  Display a help frame
    Alt-s                    │  Fill the status window
    Alt-r                    │  Read in a text file
    Alt-t                    │  Fill the text area
    Esc                      │  Clean up the display and quit
───────────────────────────────────────────────────────────────────────────

        The file-reading operation in SB_TEST demonstrates how implicit
scrolling works. The scroll is called by the sb_putc() function, called
either directly or indirectly by sb_puts().
        When used in the BIOS update mode, the screen-buffer function library
is reasonably portable. Direct mode is, of course, portable only to highly
PC-compatible computers. In the next chapter, we will look at the most
portable of the screen management tools: the ANSI device driver.



Chapter 13  The ANSI Device Driver



        DOS versions 2.00 and later can be extended to use peripheral devices
not planned for in the basic design of DOS. The use of installable device
drivers is the basis of this extendability. ANSI.SYS, the best known of
these device drivers, is supplied with DOS to handle special keyboard and
screen features. This chapter focuses on ANSI.SYS and its uses.


ANSI Basics

        The American National Standards Institute (ANSI), long before becoming
involved in efforts to standardize the C programming language, proposed
terminal and computer-equipment standards that have since become adopted by
the computer industry. Adherence to the standards is voluntary, but
terminal and computer manufacturers who choose to ignore them do so at
their own peril. Nearly all terminal equipment built since the late 1970s
conforms, more or less, to the ANSI standards.
        Conformance to the ANSI standards can be claimed as long as functions
that are implemented use the specified codes to invoke them; it is not
necessary to implement every function and capability described by the
standards. In addition, the ANSI standards permit the implementation of
private functions that are invoked by codes outside the range of those
specified. Thus, a Digital Equipment Corporation VT100 terminal, which does
not have insert- and delete-line capabilities, and which has many private
capabilities, is considered to be an ANSI-conforming terminal.
        The standard that concerns us most directly with regard to our program
designs is X3.64--1979. (The number following the dash represents the year
in which the standard became effective or was last revised.) This standard
defines a set of "additional controls" for use with the ASCII character set
defined in X3.4--1977 (a revision of X3.4--1968) and the code extensions
defined in X3.41--1974. The additional controls are invoked by using
control-function sequences and strings, which are commonly referred to as
escape sequences because the ASCII escape code (ESC, 27 decimal, 16 hex) is
used as the initial character. The escape code is the signal that
identifies the codes that follow as control information rather than
ordinary graphic symbols.
        The uses of ANSI control sequences are primarily those of screen and
keyboard handling, but other control tasks, such as switching to alternate
character sets and responding to requests for terminal type identification
("Who are you?"), are covered. The standard accommodates change with built-
in mechanisms for adding controls and with periodic review and revision as
circumstances dictate.
        ANSI control sequences have the general form shown in Figure 13-1.
The ASCII escape (shown as ESC--a single code, not three characters) is
the introducer. It is followed by a string called the intermediate
bit combinations, usually a mixture of numeric and alphabetic characters,
and a final character that represents the command to be executed.


EXAMPLE: cursor position (CUP)

                                                ESC  [20;42H
                                                ▒▒▒  ▒▒▒▒▒ ▒▒
                                                │     │    │
                                                │     │    │
            ┌───────────────────────────────────┘     │    │
            │                                         │    └─┐
            │                     ┌───────────────────┘      │
    FORMAT:   │                     │                          │
            │                     │                          │
        ESC (code 27)     (optional parameters)         (command name)
            │                     │                          │
        ┌──────┴──────┐      ┌───────┴───────┐           ┌──────┴──────┐
        │             │      │               │           │             │
        │ introducer  │      │ intermediate  │           │ final       │
        │ character   │      │ string        │           │ character   │
        │             │      │ combinations  │           │             │
        └─────────────┘      │               │           └─────────────┘
        The Escape code      └───────────────┘           The letter H is the
        introduces the       [ begins the intermediate   "cursor position"
        control sequence.    string, which includes      command.
                            the row and column
                            numbers separated by
                            a semicolon.

        FIGURE 13-1 ▐ ANSI control sequence formation


        The string in the middle of a control sequence provides variable data
that modifies the behavior of the basic command. For example, the sequence
for moving the cursor forward (CUF) is ESC[#C. The sharp sign (pound sign
or octothorpe, if you prefer) is a replaceable numeric parameter. If a
value is given, it specifies the number of columns to move the cursor. If
the value is zero or not specified, the default value of one is used.
Attempts to move past the end of the line are ignored.
        The example given in Figure 13-1 shows how to form the sequence
that moves the cursor to an absolute position on the screen. Impossible
requests are simply ignored.


Display Memory

        Figure 13-2 shows how a character and its associated attribute
are stored in a PC's display memory. In a 25-line by 80-column text
mode, each display-screen image requires a total of 4000 bytes of
memory, half devoted to characters and half to attributes. The even-
numbered byte (starting at zero) of each display-memory word holds the
code for the character. The odd-numbered byte holds the attribute.


    7     6     5     4     3     2     1     0
┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐
│     │     │     │     │     │     │     │     │ character byte (even)
└──┬──┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘
    │  ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
    │                       │
    │              ASCII character code
    │
    └──extended character flag

    7     6     5     4     3     2     1     0
┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐
│ B/I │  R  │  G  │  B  │  I  │  R  │  G  │  B  │ attribute byte (odd)
└──┬──┴─────┴─────┴─────┴──┬──┴─────┴─────┴─────┘
    │   ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒   │   ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
    │           │           │           │
    │     background color  │    foreground color
    │                       │
    │                       └────────────────────foreground intensity
    └──blink or background intensity

        FIGURE 13-2 ▐ Character and attribute bytes


        The low seven bits (0--6) of a character byte uniquely specify one
of the possible 128 character codes (0--127) defined in the ASCII character
set.
        The most significant bit of the character byte, when set (1),
indicates an extended-ASCII code, one that falls in the range 128 through
255. On an IBM PC and most compatibles, these are foreign alphabetics,
block graphics, and other special characters.
        The attribute byte is used to control monochrome and color
attributes for each character on an individual basis. As Figure 13-2
shows, bits 0 through 2 specify the foreground color, and bit 3 indicates
the intensity level of the foreground. Together, these four bits determine
the color value of the character, providing a range of 16 colors. On an
IBM monochrome adapter, only three of the values produce unique results
(0 = black, 1 = underlined, and 7 = white). All other values below 7
produce normal white, while values between 8 and 15 produce "bright"
versions of their respective values in the low range. Thus, the value 15
produces a bright white foreground.
        The background color (or grey level) of each character cell is
specified by bits 4 through 6. The background can have one of only eight
colors.
        Bit 7 serves one of two purposes depending on whether blinking is
enabled or disabled. When the blink feature is enabled, setting background
values above 7 (bit 7 set) produces quite an obnoxious blink in the
foreground of the associated character. Disabling the blink produces a far
more pleasing result: a stable background that can be any of 16 different
colors on suitable display equipment. Chapter 5 presents routines for
controlling the blink feature.


ANSI.SYS

        As mentioned previously, the ANSI.SYS file provided with DOS versions
2.00 and later is an installable device driver that provides a subset of
ANSI-standard screen and keyboard control functions. When loaded into the
computer's memory, the ANSI driver monitors keyboard input and screen
output data streams.
        For screen output, when one of the escape sequences handled by the
ANSI driver is received, the driver performs the associated action, such as
moving the cursor or setting a display attribute. During keyboard
operations, individual keys and combinations of keys can be intercepted and
converted into customized character strings. Most codes are simply passed
on without anything being done to them.
        ANSI escape sequences can be called from within our C programs or even
from DOS batch files. The SetColor program described later in this chapter
is an example of a C program that uses the ANSI driver to do most of the
work involved in controlling the attributes of a color text display.


        Installing ANSI.SYS

        Installation of ANSI.SYS is trivial. DOS uses the file CONFIG.SYS to
configure the system when it is started. If the ANSI.SYS file is in the
directory c:\dos, for example, adding the line

        device=c:\dos\ansi.sys

to CONFIG.SYS will cause DOS to load the ANSI device driver each time the
system is turned on ("cold boot") or restarted using Ctrl-Alt-Del ("warm
boot").
        A program that depends on the ANSI device driver should test for its
presence. If the ANSI driver is not loaded into memory, the program should
abort and provide instructions to the user about installing it. A method of
testing for the presence of the device driver is presented in the next
section of this chapter.


        The ANSI Control Codes

        Figure 13-3 (on the next page) summarizes the ANSI standard
control capabilities and indicates which are supported by the DOS
ANSI device driver. Note that some of the supported capabilities (for
example, erase in display, ED) are implemented in a simplified way, and
that others (for example, set mode, SM, and reset mode, RM) are decidedly
implementation-dependent, being tied intimately to the IBM PC hardware
architecture.
        The following descriptions are intended to supplement the
information presented in the documentation provided with DOS, particularly
with respect to usage guidelines. The subsequent section of this chapter
develops an interface package to the ANSI driver and provides practical
examples of these codes in use.

    CONTROL SEQUENCES WITH NUMERIC PARAMETERS

╓┌─────────────┌───────────┌─────────────────────────────────────────────────╖
    ANSI.SYS    Mnemonic    Description
    ANSI.SYS    Mnemonic    Description
───────────────────────────────────────────────────────────────────────────
    No          CBT         Cursor Backward Tabulation
    No          CHA         Cursor Horizontal Absolute
    No          CHT         Cursor Horizontal Tabulation
    No          CNL         Cursor Next Line
    No          CPL         Cursor Preceding Line
    Yes         CPR         Cursor Position Report
    Yes         CUB         Cursor Backward
    Yes         CUD         Cursor Down
    Yes         CUF         Cursor Forward
    Yes         CUP         Cursor Position
    Yes         CUU         Cursor Up
    No          CVT         Cursor Vertical Tabulation
    No          DA          Device Attributes
    No          DCH         Delete Character
    No          DL          Delete Line
    No          ECH         Erase Character
    No          FNT         Font Selection
    No          GSM         Graphic Size Modification
    No          GSS         Graphic Size Selection
    ANSI.SYS    Mnemonic    Description
    No          GSS         Graphic Size Selection
    No          HPA         Horizontal Position Absolute
    No          HPR         Horizontal Position Relative
    Yes         HVP         Horizontal and Vertical Position
    No          ICH         Insert Character
    No          IL          Insert Line
    No          NP          Next Page
    No          PP          Preceding Page
    No          REP         Repeat
    No          SD          Scroll Down
    No          SL          Scroll Left
    No          SPI         Spacing Increment
    No          SR          Scroll Right
    No          SU          Scroll Up
    No          TSS         Thin Space Specification
    No          VPA         Vertical Position Absolute
    No          VPR         Vertical Position Relative


    CONTROL SEQUENCES WITH SELECTIVE PARAMETERS

╓┌─────────────┌───────────┌─────────────────────────────────────────────────╖
    ANSI.SYS    Mnemonic    Description
─────────────────────────────────────────────────────────────────────────
    No          CTC         Cursor Tabulation Control
    No          DAQ         Define Area Qualification
    Yes         DSR         Device Status Report
    No          EA          Erase in Area
    Yes         ED          Erase in Display
    No          EF          Erase in Field
    Yes         EL          Erase in Line
    No          JFY         Justify
    No          MC          Media Copy
    No          QUAD        QUAD
    Yes         RM          Reset Mode
    No          SEM         Select Editing Extent Mode
    Yes         SGR         Set Graphic Rendition
    Yes         SM          Set Mode
    No          TBC         Tabulation Clear

        FIGURE 13-3 ▐ ANSI control sequences


        Cursor Position (CUP)  ESC[#;#H
        Horizontal and Vertical Position (HVP)  ESC[#;#f
        These two control sequences do the same job. They move the cursor to
the position designated by the parameters: the row (the first #) and the
column. Illegal requests are ignored. The upper left corner of the screen
is designated row 1, column 1. Missing or zero-valued parameters default to
1; therefore, a CUP or HVP sequence with no stated parameters is a synonym
for "homing" the cursor.

        Cursor Up (CUU)  ESC[#A
        Cursor Down (CUD)  ESC[#B
        Cursor Forward (CUF)  ESC[#C
        Cursor Backward (CUB)  ESC[#D
        Each of these control sequences moves the cursor by # positions (the
default is 1) in the specified direction. Attempts to move beyond either
end of a row or off the top or bottom of the screen are ignored. For
example, if a request to move the cursor down exceeds the number of rows
between the one containing the cursor and the bottom row of the screen, the
cursor is deposited on the bottom row. No scrolling occurs.

        Device Status Report (DSR)  ESC[6n
        Save Cursor Position (SCP)  ESC[s
        Restore Cursor Position (RCP)  ESC[u
        These three control sequences are designed to assist in cursor
control. The first two determine where the cursor is located, and the third
returns the cursor to a saved location. The DSR request causes the ANSI
driver to place a string, in the form ESC[#;#R plus a trailing carriage
return, into the keyboard buffer. The position parameters are stored as
two-character representations of the numbers, so the string contains a
total of nine characters. The ansi_cpr() function described in the next
section can be used to retrieve the row and column data.
        The SCP/RCP pair of control sequences can be used to save a cursor
position so that it can be restored after an intervening operation moves
the cursor from the saved position. Only one position can be saved for
subsequent restoration. If multiple calls are made to SCP, a call to RCP
will restore the cursor position obtained by the most recent call to
SCP.

        Erase in Display (ED)  ESC[2J
        Erase in Line (EL)  ESC[K
        The ED control sequence erases the entire screen by displaying blanks
in the prevailing video attribute (see SGR). The cursor is deposited at the
upper left corner of the screen. EL erases the characters from the cursor
position to the end of the current row. The "erased" positions are set to
blanks in the prevailing attribute, and the cursor is left at its current
position.

                                CAUTION
            Some versions of the ANSI device driver provided with
            MS-DOS (for example, MS-DOS 2.11 for the AT&T PC6300)
            erase by setting characters to blanks in the normal
            (white on black) attribute rather than the attribute
            set by SGR.

        Set Graphic Rendition (SGR)  ESC[#; ... ;#m   A graphic rendition is
the visual style of displaying a graphic symbol (number, letter,
punctuation mark, and so forth), which in the IBM PC context translates
into the attribute associated with a character. The ANSI driver uses the
SGR control sequence to set the intensity, blink status, and color
attributes of characters on a color adapter. On a monochrome system, SGR
can be used to set intensity, blink status, and underlining. Some IBM-
compatible machines (COMPAQ and AT&T are two examples) permit grey-scale
settings on monochrome monitors in response to color attribute requests.
        SGR can take a series of numeric parameters in a single call. The
effect of a single call to SGR with multiple parameters is the same as that
produced by a series of calls to SGR with one parameter per call, but the
former will be faster because of reduced function-call overhead.

        Set Mode (SM)  ESC[=#h
        Reset Mode (RM)  ESC[=#l
        The mode control sequences that are supported by the ANSI driver
set the IBM PC and compatible hardware-dependent video modes for the
color/graphics adapter and a "wrap-at-end-of-line" mode. The driver does
not support switching to a monochrome adapter from a color/graphics
adapter. Both SM and RM can be used to set a video mode with the parameters
0 through 6. Setting a different video mode clears the current mode. Using
SM with a parameter of 7 enables wrap; RM given the same value disables
wrap.


        Pros and Cons of ANSI.SYS

        The primary reasons for using the ANSI device driver are simplified
programming and portability of programs to other versions of MS-DOS and
PC-DOS and to the machines that run DOS. The amount of programming effort
required to do many screen operations is reduced to simple calls to ANSI
control sequences. Compare the short cursor-positioning macro shown in the
next section to the BIOS-interrupt-based function presented in Chapter 5;
both do the same job.
        Unfortunately, some limitations of the ANSI driver make it unsuitable
for many applications. For some purposes--full-screen text editing is a
good example--it is simply too slow. In addition, ANSI.SYS implements only
a very limited subset of ANSI X3.64. It has no concept of fields, protected
display areas, display regions (useful for windowing), and many other
important capabilities.


An ANSI Interface Package

        In spite of its limitations, the ANSI driver has its place. It
comprises a useful but limited subset of ANSI capabilities that can do a
surprising amount of work. The interface to the ANSI driver described here
uses mostly macros in a header file to accomplish the tasks of moving the
cursor, finding its position, setting video attributes, clearing the
display, and setting video modes. A few supporting C functions built on the
ANSI driver take care of some higher level tasks, such as testing to see
whether the driver is installed.
        The macros and definitions of the video attributes and modes are
contained in the header file ansi.h, which resides in the \include\local
directory. You should #include the file in any C program or function that
will need to access the keyboard or screen through the ANSI device
driver.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 358 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        ANSI color attributes follow a different sequence from the standard
IBM attribute set. The ansi.h file contains definitions for the basic
colors and "shift" values (offsets) for foreground and background attribute
specifications. In addition, there are definitions for normal, bright,
blink, reverse, and invisible attributes. The normal attribute effectively
clears all other special attributes. Thus, to set the foreground to bright
green, one uses the statements

        ANSI_SGR(ANSI_GREEN + ANSI_FOREGROUND);
        ANSI_SGR(ANSI_BRIGHT);

which will cause all subsequent characters sent to the screen to be
displayed in the requested attribute until a new specification is
given.
        A typedef is used to create a POSITION data type that is used by some
of the functions in the SetColor program to keep track of which color
attribute position (foreground, background, or border) is being attended to
in the processing of attribute specifications. Each position must be
handled in its own special way, as we'll see shortly.
        In addition to the macros defined in ansi.h, our interface employs
several functions. The ansi_cpr() function is used to request a device
status report (ANSI_DSR) and then to read the cursor-position data stored
in the keyboard buffer. The format of the response is a string of nine
characters, starting with ESC and ending with a carriage return. This is
called the cursor position report, hence the name ansi_cpr() for the
function.
        The ansi_cpr() function calls getkey(), a DOS interrupt-based routine
(INT 21H) that was defined in Chapter 5, to gather input from the keyboard
buffer. Here is the code for ansi_cpr():


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 361 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        Because the characters are placed in the buffer in a reasonable order,
successive keyboard read calls can be used to gather and process the
numeric values easily. We must read every character in the string, even
though we're really interested in only four of them, so that no residue is
left behind to befuddle other parts of our program's input processing. The
formal parameters row and col are pointers to storage outside the function.
        A character representation of the tens digit of a number (row or
column) is obtained by a call to getkey(). It is converted to its numeric
representation by subtracting 0 (decimal 48) and then multiplying by ten
(its numeric weight). The value is then added to the converted value of the
next character obtained by getkey(), which is the units digit, to produce
the needed number in integer format.
        We now have the ability to determine the cursor's position and to save
it or pass the information on to other parts of our programs. But, if the
ANSI device driver is not already loaded into memory, calling ansi_cpr()
will cause the computer to appear to lock up. We need to be sure the driver
is installed before calling functions that depend on its presence. The
function ansi_tst() calls ANSI_CUP, which sets the cursor to a known
location, and then calls our bios library function readcur() to read the
cursor location. If the row and column positions match, the driver is
probably loaded. I say "probably" because the routine will falsely claim
the driver is loaded if the cursor happens to be at the test location
before the ANSI_CUP call. (However, this is not likely for the test values
used.) To be certain, a series of two tests at differing locations could be
used.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 362 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        The code for ansi_tst() aborts with an error message and instructions
to the user about installing the ANSI driver. An alternative design might
simply return a true/false response and let the program deal with the lack
of a driver in some other way, such as switching to a mode of operation
that does not depend on the ANSI driver being loaded. The file ansi.mk is
the makefile to compile the separate components of the ANSI interface
package.

    # makefile for the ANSI library

    LINC=c:\include\local
    LLIB=c:\lib\local

    ansi_cpr.obj:   ansi_cpr.c $(LINC)\ansi.h
            msc $*;
            lib $(LLIB)\ansi -+$*;

    ansi_tst.obj:   ansi_tst.c $(LINC)\ansi.h $(LINC)\video.h
            msc $*;
            lib $(LLIB)\ansi -+$*;


The SetColor Program

        Now we'll do something useful with the ANSI interface. SetColor,
invoked as SC to save keystrokes (in the UNIX tradition), is designed to be
used with a computer that has a color display system. With minor
modifications it can be used in a limited way with monochrome systems,
too.
        Figure 13-4 is the manual page for SetColor. The program
sets foreground and background attributes via ANSI control sequences. The
ANSI driver provides no way to control the border attribute, so we resort
to a BIOS routine, palette(), developed in Chapter 5, to do that job. The
use of palette() may make the program nonportable to some nominally IBM-
compatible equipment.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the figure found on page  ║
                ║ 364 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝

        FIGURE 13-4 ▐ Manual page for SetColor


        The following pseudocode description of SetColor reveals that the
program exhibits extreme schizophrenia. On the one hand, it operates in an
interactive mode when invoked as SC without arguments. The user selects
attributes by pressing function keys. Screen updates immediately show the
user what the combined attribute selections look like. However, when called
with attribute specifications as arguments, SetColor runs in batch mode.
All attributes and intensity modifiers, if any, are taken from the command
line. In this mode, SetColor can be used to control screen appearance from
within DOS batch files.

    if ANSI driver not loaded
            print message
            exit
    if not in color text mode
            print message
            exit
    if command-line argument given
            run in batch mode
    else
            run in interactive (menu) mode
    clear the screen
    exit

        The SetColor program consists of several functions, each packaged in
its own file. Some of the functions have general applicability to other
tasks and are, therefore, placed in local libraries.
        Starting at the top, the source file sc.c contains the main()
function. In main(), two tests are made that determine whether the SetColor
program runs to completion or quits early. The first test, ansi_tst(),
checks to see that the ANSI device driver is installed. If not, the program
terminates. If the driver is installed, the second function, iscolor(),
finds out what video mode is in use. Rather than do anything fancy at this
point, we opt to simply terminate program execution with a help message if
the mode is not a standard color-text mode. The user's selection of batch
or interactive operation, based upon the presence or absence of command-
line arguments, is also detected here.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 367 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        If attribute arguments are given, the function parse() is called with
the argument list obtained by main(). The job of parse() is to analyze each
argument and form attribute or color specifications that can be processed
by the low-level ANSI interface functions. If no arguments are given, the
menumode() function is called. Let's first examine the batch processing
functions.


        Batch Mode

        If parse() receives a single attribute specification, it checks to see
whether it is one of the special attributes (normal, reverse, invisible, or
blink). None of these takes an optional intensity modifier, and each can be
handled easily by a single call to ANSI_SGR. If the lone argument is not
one of these special attributes or if there are two or more arguments,
parse() falls through to a loop that analyzes each argument in turn and
creates attribute specifications in a form suitable for processing by the
ANSI interface functions.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 368 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        The basic problem we face is receiving color/attribute specifications
as text strings and converting them to numbers. We will also need to
account for the differences between the standard IBM attribute numbers and
the ANSI attribute numbers described earlier. The IBM attribute numbers are
defined in the header file ibmcolor.h, which resides in the \include\local
directory.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 370 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        The colornum() function converts text strings into these IBM color/
attribute numbers. Given the string green as an argument, for example,
colornum() returns the defined value IBM_GREEN, which is the number 2. Two
synonyms for bright (bold and light) are allowed, and yellow is treated as
a synonym for bright brown. The following is the source code for colornum():


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 371 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        The function uses a simple sequential search through a table of
string/number pairs that compares the first three letters of the string
argument with the table entries. The strlwr() standard library function
converts the incoming argument to lowercase before any comparisons are
made. Its return type is cast to void because the pointer to a character is
not used. If no matching string is found, colornum() complains by returning
-1 instead of a valid color number.
        A color number thus produced may be passed as an argument to
setattr(), which takes a position indicator and an attribute value as
input. It outputs an ANSI control sequence by applying the ANSI_SGR macro
to a properly converted attribute number. The process sounds more confusing
than it really is. First, the code for setattr():


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 373 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        The pos argument has type POSITION (defined in the ansi.h header
file), and it tells setattr() which rules to apply to the attr argument.
The attribute argument contains two kinds of information lumped together:
The low three bits (0--2) represent the base color or video attribute, and
bit 3 indicates the intensity.
        An intensity modifier is treated differently when it is applied to a
foreground attribute than when it is applied to a background attribute. In
the foreground case, bit 3 of the intensity modifier sets the foreground
intensity bit in the attribute byte (bit 3). In the background case, it
sets the blink bit (bit 7). If blinking is enabled (the default), setting
the blink bit causes the foreground to blink and does not change the
background intensity, resulting in a useful range of eight background
colors. If blinking is disabled, setting the blink bit causes the
background intensity to be bright. The full range of 16 background colors
can be obtained with blinking disabled. The setattr() function calls
ANSI_SGR once if attr is for normal intensity or twice for high
intensity.
        A table of foreground and background attribute values in the array
ibm2ansi[] contains the conversions from IBM attributes--represented by the
index into the array--to the ANSI values needed by ANSI_SGR. Thus, the
expression ibm2ansi[IBM_BLUE], where IBM_BLUE is defined in
ibmcolor.h as the number 1, yields the value represented by ANSI_BLUE,
which is defined in ansi.h as the number 4.
        The border, on display systems that support it, can be set to any of
16 colors by using palette(). There is no need to separate the intensity
information from the rest of the attribute when setting the border because
palette() is a BIOS-based function and uses the IBM attribute values
directly.
        That explains batch-mode processing. The other processing path that
can be taken from main() is interactive mode, which is handled by
menumode(). The menumode() function calls upon palette(), setattr(),
getkey(), and a new function, sc_cmds(), to do the bulk of the work.


        Interactive Mode

        In interactive mode, attributes are selected by pressing function
keys. F1 decrements the foreground color and F2 increments it. The color
values wrap around at the boundaries; therefore, a request to increment
foreground = 15 (white) produces foreground = 0 (black). Similarly, the
F3/F4 pair of function keys are used to select the background attribute,
and the F5/F6 pair cycle through border attributes.
        The mainmenu() function returns control to DOS when the Return key is
pressed, leaving the screen cleared to the most recently selected attribute
combination.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 375 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        Because the color/attribute specifications are presented in
interactive mode as numbers rather than as text strings, setattr() can
process them directly. The process of updating the screen after each set of
calls to setattr() means that the list of commands and the current status
information must be rewritten following each keypress that affects the
foreground or background color. This is done by the function sc_cmds(),
which contains a static array that converts IBM color numbers into text
strings suitable for humans. All cursor positioning is done via the
ANSI_CUP macro. Text strings are written to the display by the standard
library function fputs(), which is directed to send its output to
stdout.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 377 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        The file sc.mk is the makefile for SetColor. It collects several of
the interface functions into a special-purpose library, color.lib. This is
done to minimize the number of items that must be specified on the
dependency and link lines that create SC.EXE and keep it up-to-date. A
linker-response file could be used to achieve the same result. To rebuild
the SetColor program after any changes are made to its source files or
libraries, type

        make sc.mk


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 378 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        Using SetColor

        Aside from being a good demonstration of the ANSI device-driver
interface, SetColor is a useful program. I use it most frequently in batch
files to restore the screen to match my peculiar tastes after some other
program has made a mess of things. Most programs, especially those that do
full-screen operations, have built-in default settings for video
attributes. These programs have no idea what attributes were in use before
they were run; therefore, when they exit, they either leave the attributes
as they are when the program terminates or set them to white on black.
        It would be better to have the colors set to the user's preferences,
if possible. This can be achieved in a couple of ways. The first way uses a
DOS batch file to intercept the call to a program. The batch file calls the
real program and then calls SetColor to make things right again. For
example, assume the program in BADPROG.EXE does bad things to the screen
when it exits. A batch file called BADPROG.BAT with the lines

        badprog.exe
        sc white blue blue

will clear the screen and set the foreground to white on a field of blue
when the program returns to DOS.
        This can be generalized further by the use of DOS environment
variables. If environment variables having the names FGND, BKGND, and
BORDER are defined, the batch file can access the values and control the
SetColor program automatically. DOS variable values are obtained in batch
files by surrounding each variable name with percent signs. The revised
batch file looks like this:

        badprog.exe
        sc %fgnd% %bkgnd% %border%

        Of course, SetColor may be called from the DOS command line with
literal attribute specifications. I sometimes change the attributes just to
minimize eyestrain during long programming and writing sessions. It's
remarkable what a good effect a simple change in screen colors can have
(unless it's to something like magenta on red!).


        A User-Attribute Function

        Here's a way for programmers to be kind to those who will be using
their programs. The function userattr() that follows can be used in
programs to restore the user's attribute preferences just before the return
to DOS. It gets values directly from the DOS environment, just as the batch
file in the previous section does. If no attribute variables are defined,
userattr() uses some reasonable defaults as parameters. The userattr()
function calls on colornum() and setattr() and uses the standard library
functions getenv() and strtok() to look for user preferences and parse them
into recognizable values.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 380 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        The processing of the strings by strtok() is necessary because the
attribute specifications may contain an intensity modifier. The strtok()
function returns a pointer to a null-terminated string token taken from its
first string argument; if no tokens are found, it returns NULL. Acceptable
token separators are spaces and tabs, in any combination. Subsequent calls
to strtok() use NULL as the first argument to request further searching in
the string given in argument one of the first call. If the first call to
strtok() retrieves an intensity modifier, a second call is made to strtok()
on the same string to get the second token, which should be a valid
attribute specifier. Error-checking code prevents bad attribute names from
causing problems.
        The next time someone tells you the ANSI.SYS device driver is a
worthless piece of software, you can show them how useful it really
is!
        As a fitting end to this development session, echo ESC[2J (look it
up).



Chapter 14  Viewing Files



        The next program to be added to our kit of tools is a file viewer.
ViewFile, called VF by its close friends and associates, is such a
tool.
        The design for VF has two objectives: to obtain a convenient window
into a file that permits us to look at the file without harming it and to
experiment with some programming techniques that we can apply to other
tasks. We will apply the linked-list techniques to the management of an in-
memory text buffer. In addition, we will use the standard library function
strncmp() to implement a simple but fast search capability.
        Some trade-offs made in VF's design are noteworthy. First, we
sacrifice loading speed to obtain very quick response to user commands. By
using an internal buffer, instead of buffering to disk, we provide speedy
response to positioning requests but limit files to a size determined by
the available memory of the host machine. Fortunately, most PCs these days
have lots of memory. Second, VF uses some of the BIOS routines described in
Chapter 5 to handle display interactions. It does not use the buffered
screen interface described in Chapters 11 and 12. Even so, the display
updates are tolerably quick, taking about a second or less for most
operations on a standard PC for a typical file.


ViewFile (VF) Features

        The features of ViewFile are summarized in the manual page below
(Figure 14-1).


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the figure found on page  ║
                ║ 384 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝

        FIGURE 14-1 ▐ Manual page for ViewFile


        In addition to offering vertical scrolling by screen pages, VF
provides convenient horizontal scrolling as a way of dealing with long
lines. VF accepts lines up to MAXLINE characters (defined as 256 in our
std.h header file) including the null-byte terminator. You can make this
value larger if you wish. The horizontal scrolling feature lets you move
right and left, 20 columns at a time. The horizontal offset is maintained
when vertical movements are made.
        If a list of files is presented to VF, the program will view the files
sequentially. Pressing the Escape key tells VF to read the next file in the
list, if any. To quit without viewing any remaining files in the list, type
Q instead. DOS wildcards may be used to specify ambiguous file names.
Therefore, the command

        vf \src\*.h

forms a list consisting of all header files in the \src directory on the
current disk drive. VF then reads in the first matching file and displays
the first screen page of its contents.
        The forward and reverse search feature looks through each line in the
buffer for a literal string. VF searches around the end of the buffer,
stopping at the first line that contains a match, or returning to the
current line if no match is found. Searches in either direction may be
repeated by pressing a carriage return in response to the "Search for:"
prompt.
        VF has a built-in help "frame." When a user types a question mark,
Alt-h, or H, VF overlays the help frame on the text-display area. The next
keypress restores the covered text. Although it is only a brief memory-
jogger, the help frame contains all the information needed to use VF
successfully.
        VF should be compiled using the compact memory model so that it can
handle large files. Compact model programs are distinguished by small code
space (less than 64 KB) and large data space (whatever available memory
permits). Files that exceed the capacity of VF produce the message "File
too big to load." To use the compact memory model, you will have to create
compact-model versions of the bios, dos, and util libraries by recompiling
all functions with the -AC option. It is probably best to follow the
pattern established by Microsoft of having separate library files for each
version of a given library. Thus, for example, we might have the files
bios.lib, cbios.lib, lbios.lib, and so on. The linker gets clues from the
object files, so it will use the correct versions of the standard
libraries. The makefile, vf.mk, contains the instructions required by MAKE.
The library of object modules used to construct VF is maintained by vf.mk,
which enables us to bypass the command-line length limitation imposed by
DOS.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 386 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        The initialization file tools.ini in the VF directory instructs MAKE
to verify the number and types of function arguments and to use the compact
memory model. Renaming or deleting this file causes the default small-model
tools.ini file to be read for inference rules.

    # inference rules for VF program make
    # enable argument checking and compact memory model

    [make]
    .c.obj:
            msc -DLINT_ARGS -AC $*;


Implementation Details

        VF is a modest-sized program. Some of its general-purpose functions
are available to other programs.
        VF uses an in-memory text buffer. The decision to use an in-memory
buffer has a profound impact on how a few of the functions are designed,
but virtually no impact on the rest. We should try to keep the number of
functions that actually know how the text is stored to a minimum; this
permits us to apply them to a wider range of problems. The functions in the
message module message.c, for example, don't know anything about the text
buffer. Therefore, these message functions can be used unchanged in any
program that uses the BIOS video routines.
        The functions in vf.c are also ignorant of the buffer mechanism. The
main() function does the customary work of preserving the user's
preferences for display type, video attributes, and cursor type. It also
aborts the program if the operating environment is a DOS version earlier
than 2.00, because VF is designed to accept pathnames in file
specifications.
        The main() function also clears the screen and sets up the basic
display appearance for the VF session. All program and status information,
user input, and feedback messages are confined to the top two lines of the
screen. The remaining 23 lines are used to display text and optional file-
relative line numbers. When help is requested, the help frame is
temporarily overlaid on a rectangular portion of the text display area.
        Because the cursor is turned off by VF (except during user input
operations), it is necessary to restore its shape when the program ends.
The clean() function in vf.c is called when the program terminates normally
or when some condition (out of memory, for example) causes an abnormal
termination. The user's original video attribute selection is restored by
clean(), and the cursor is then sent to the home position.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 388 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        A set of manifest constants that controls placement of visual elements
of the VF display, search direction, and the data structure that defines
the text buffer are contained in vf.h. The buffer data structure and its
application will be described in detail later. The constants determine
where the program name and messages to the user appear, which screen line
is the "header" row, and how many screen rows of text display and scrolling
overlap are to be used. To simplify VF a bit, we do not add automatic
configuration to the program. The information and samples in Chapters 7
and 9 give detailed information about configuring programs.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 391 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        For each file named on the command line (either literally or
ambiguously), main() calls vf_cmd(), the ViewFile command module. The
vf_cmd.c file, which contains the source for vf_cmd() and for the auxiliary
function prtshift(), is a large file but it does only a few jobs--all
related to controlling and accessing the text buffer. Chief among these
jobs is setting up the text buffer for the file that is about to be read.
Another major job is handled by the big switch statement within the while
loop in vf_cmd(), which accepts keypresses as commands. Simple positioning
commands are handled directly by vf_cmd(). Commands that require user input
are handled by separate functions.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 392 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        Within vf_cmd() there are several calls on putstr(). This is a BIOS-
based function that displays a string in the prevailing video attribute on
a specified screen page. It advances the cursor by the number of characters
written. (We could also use the BIOS writetty() function to help us with
this task, but writetty() disturbs the video attribute, setting it to the
default, which may not be what we want.)


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 399 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        As noted earlier, the vf.h header file contains the definition of the
buffer node structure. In addition to having forward and backward pointers
to other nodes, each node contains a pointer to a character string, a file-
relative line number, and a "flag" word. The flag word is not used in this
implementation of VF, but it could be used to mark lines for highlighting.
        The text buffer management functions are in vf.list.c:


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 399 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        The text buffer used by VF consists of three parts: a group of
"control nodes" that form a doubly linked list, pointers to the buffer
"head" and the "current" position, and independent character strings for
each line in the file being viewed. The node pointed to by head is a dummy
node that is used to ease the work necessary to set up and maintain the
list. Figure 14-2 depicts the text buffer. Note that the control nodes
are in a circular list, which makes searching across the buffer
boundaries easy. It takes the same amount of time to move backward in
the buffer as it does to move forward, because of the use of a doubly
linked list. Most other text buffer designs would exact a rather stiff time
penalty for scrolling backward in a file.
        Figure 14-3 shows another aspect of the text buffer-allocation
scheme used by VF. An initial pool of 256 "free" nodes is allocated by a
call to vf_alloc(); the free nodes are controlled by the symbolic
constant N_NODES. The list-header node is established by vf_mklst().
Both vf_mklst() and vf_alloc() call the library function malloc()
to obtain needed storage.


                control nodes
                    │
                ▒▒▒▒▒▒▒▒▒▒▒▒▒▒                 text string buffers
┌─head ┌────────┐                                   │
│      │ ┌───┐  │               ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
│┌──┐  │ │  ┌▼─┬┼─┬─────┬──┐
└│■─┼──┼─┼─►│■ │■ │     │■ │
    └──┘  │ │  └┼─┴─┴─────┴──┘
    ┌──┐  │ │  ┌▼─┬┼─┬─────┬──┐   ┌───────────────────────────────────────┐
┌┤■─┼──┼─┼─►│■ │■ │     │■─┼──►│                                       │
│└──┘  │ │  └┼─┴─┴─────┴──┘   └───────────────────────────────────────┘
│      │ │  ┌▼─┬┼─┬─────┬──┐   ┌──────────────────────────────┐
└─cur- │ │  │■ │■ │     │■─┼──►│                              │
    rent │ │  └┼─┴─┴─────┴──┘   └──────────────────────────────┘
        │ │  ┌▼─┬┼─┬─────┬──┐   ┌──────────────────────────────────────────┐
        │ │  │■ │■ │     │■─┼──►│                                          │
        │ │  └┼─┴─┴─────┴──┘   └──────────────────────────────────────────┘
        | |   |  |
        │ │  ┌▼─┬┼─┬─────┬──┐   ┌────────────────────────────┐
        │ │  │■ │■ │     │■─┼──►│                            │
        │ │  └┼─┴─┴─────┴──┘   └────────────────────────────┘
        │ └───┘  │
        └────────┘

        FIGURE 14-2 ▐ Text buffer management


                ┌────────┐
                │ ┌───┐  │
        ┌──┐ │ │  ┌▼─┬┼─┬────────┐
    head│■─┼─┼─┼─►│■ │■ │        │ Inserting a node into the list:
        └──┘ │ │  └┼─┴─┴────────┘
                │ │  ┌▼─┬┼─┬────────┐ if (insertion will exhaust the
                │ │  │■ │■ │        │     freelist)
                │ │  └┼─┴─┴────────┘ extract first node from the freelist
                │ │  ┌▼─┬┼─┬────────┐ insert the node after the "current" node
                │ │  │■ │■ │        │ return a pointer to the next free node
                │ │  └┼─┴─┴────────┘
                │ │  ┌▼─┬┼─┬────────┐
                │ │  │■ │■ │        │
                │ │  └┼─┴─┴────────┘
        ┌──┐ │ │  ┌▼─┬┼─┬────────┐
    current│■─┼─┼─┼─►│■ │■ │        │
        └──┘ │ │  └┼─┴─┴────────┘
                │ └───┘  │
                └────────┘ ◄───────────────┐
                                        │
        ┌──┐      ┌──┬──┬────────┐      │
freelist│■─┼─────►│■ │■ │        │ ►────┘
        └──┘      └┼─┴──┴────────┘
                    ┌▼─┬──┬────────┐
                    │■ │■ │        │
                    └┼─┴──┴────────┘
                    ┌▼─┬──┬────────┐
                    │■ │■ │        │
                    └┼─┴──┴────────┘
                    ┌▼─┬──┬────────┐
                    │■ │■ │        │
                    └──┴──┴────────┘
                    |
                    |
                    ┌▼─┬──┬────────┐
                    │■ │■ │        │
                    └──┴──┴────────┘

        FIGURE 14-3 ▐ Buffer-allocation scheme


        The header-node pointers are both initially set to point to the header
node itself. This constitutes the empty list. The freelist pointer points
to the chained free nodes. Only the "next" pointers are used for chaining,
and the last free node contains a NULL next pointer. Each line of text is
effectively added to the buffer by the function vf_ins(), which inserts a
free node into the list after the position of the current node. When the
supply of free nodes is about to be exhausted, another block of nodes is
allocated and added to the chain. This process continues as long as there
are more lines to read from the file and there is enough memory to satisfy
the allocation requests.
        Looking at vf_cmd.c, we see that after a new node has been inserted
into the list by vf_ins(), there is still more work to be done. The
Microsoft C standard library function strdup() is used to duplicate the
text of the most recently read line from the file. This function also calls
malloc() to obtain an array of characters that is large enough to
hold a copy of the received string, including the terminating null
byte.

                                COMMENT
            Since strdup() is not a UNIX System V library function,
            many C compilers may not have it or an equivalent
            function in their run-time support libraries. You can
            easily mimic the function using malloc(), strlen(), and
            strcpy():

            #include <stdio.h>
            #include <malloc.h>
            #include <string.h>

            char *
            strdup(str)
            char *str;
            {
                    char *buf;

                    buf = malloc(strlen(str) + 1);
                    return (buf == NULL ? buf : strcpy(buf, str));
            }

        We are careful to check for error returns that let us know if memory
is exhausted. VF simply cleans up and quits with an error message if it
runs out of memory.
        The function vf_del() takes a node out of the list and puts it back on
the freelist. We call vf_del(), and free(), a standard library function,
when the command loop is terminated by a "next file" (Escape) command. This
frees the memory used by the current file, leaving the largest possible
amount for the next file in the list. If we do not free the memory, we
might eventually receive a "File too big to load" error message.
        After any command that affects the current buffer position, vf_dspy()
is called to update the text display area of the screen. The version of
vf_dspy() presented here uses the bios library functions that we
constructed in Chapter 5 to position the cursor, control display
attributes, and display text and optional line numbers. I elected to use
global variables only for the video subsystem based on the BIOS interface.
The Boolean variable numbers defaults to FALSE but may be set to TRUE
for the duration of a VF session by using the command-line option -n. The
numbers variable controls the displaying of line numbers. The initial
setting of numbers is passed as an argument from main(), relayed through
vf_cmd(), to vf_dspy(). The user may toggle line-numbering on and off while
viewing a given file, but the state of line-numbering goes back to the
initial setting when the next file from the list is brought into the
viewing buffer.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 405 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        The vf_dspy() function updates the text-display area by calling
putfld(), which outputs a string of characters to a field starting at the
current cursor position. The putfld() function uses a compression technique
that is made possible by BIOS. All sequences of repeated characters are
output by a single call to writec(). This technique reduces the time needed
to update a screen that contains, for example, the large amounts of white
space found in C and Pascal source files. Assembly-language source
listings, on the other hand, usually do not benefit from this compression
technique because they contain long lines and fewer repeated character
sequences.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 407 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        This function becomes part of the bios library described in
Chapter 5. It can be used by any program that uses BIOS to access the PC
screen.
        We have to watch out for the boundary conditions imposed by physical
realities. When there are not enough lines left in the buffer to fill the
screen, vf_dspy() must be careful not to leave residue from previous
displays. After displaying the last line of the text buffer, vf_dspy()
clears each remaining screen line and deposits a tilde in the leftmost
column to show that the line is an unused screen line, as opposed to an
empty buffer line.
        Two utility functions that are specific to VF reside in vf_util.c. The
first is gotoln(), which implements the absolute go-to-line feature. This
function prompts the user for a line number. If the number is within the
range of lines in the buffer, the current position is updated and the line
sought by the user is brought to the top of the text display area.
        The second function is called showhelp(). To assist the user with a
brief memory-jogger of command names and actions, showhelp() displays the
essence of the manual page at the top of the text display area. When the
user has finished reading the help frame, a keypress restores the screen to
its former condition by rewriting all of the text lines.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 408 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        The task of gathering the user's input for the gotoln() function falls
to getstr(), a function that is constructed from several of the dos and
bios library functions presented in Chapter 5. The getstr() function takes
buffer and width parameters as arguments. It echoes keystrokes as it
receives them and interprets a few keys as commands rather than as input.
The user cancels input by pressing the Escape key, and signals the end of
input with a carriage return. Errors may be corrected by pressing Ctrl-H
(backspace). All input is in the form of character strings. If a program
requires numeric input, such as a line number, it must convert the string
to numeric form before using it.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 411 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        Notice that getstr() is actually a simplified version of the
getreply() function that we used in Chapter 6. We don't need a sliding
window or other fancy gimmicks, so we took out all the superfluous
windowing and editing features, made the prompting external, and eliminated
the response stack. What's left is a simple but effective function that
collects a string from the keyboard. Two boundary conditions are addressed:
First, we don't allow the user to backspace beyond the left column of the
response field, and second, we return the response immediately if the user
types past the right column of the response field. You may prefer to
modify the latter condition to require an explicit carriage return before
returning the response.
        Additional assistance is provided to the user via the message line on
the right half of the top screen line. The simple message-line manager
consists of three functions contained in message.c. The function initmsg()
establishes the message display area by filling in the values called for by
the MESSAGE data structure defined in the message.h header file. The
message line can be placed anywhere on the screen by changing the values
presented to initmsg(). The showmsg() function displays a message, and
clrmsg() restores the message area to its pristine state. A flag in the
message structure is set when a message is displayed and is reset when the
message area is cleared. If a call is made to clrmsg() and the flag is not
set, no action is taken.

    /*
    *      message.h -- header for message-line manager
    */

    typedef struct msg_st {
            int m_row;
            int m_col;
            int m_wid;
            int m_pg;
            int m_flag;
            unsigned char m_attr;
    } MESSAGE;



                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 413 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        The message-line manager functions are based on bios library routines.
They can be generalized to use either standard library routines or the
screen-buffer routines described in Chapters 11 and 12, which permit
their use in programs that do not use the BIOS interface.
        A simple but useful search feature is implemented by the functions in
vf_srch.c. The getsstr() function prompts for a search string and reads it
using getstr(). The string must be literal in this implementation. The
string is presented to search(), which tries to find a line in the
specified search direction that contains a match for the search string.
Searching starts on the line following the current line (or the preceding
line for a reverse search) and continues until a match is found or until
search() returns to the current line. The search is circular because of the
way the buffer is implemented.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 415 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


    /*   nlerase -- replace the first newline in a string
    *   with a null character  */

    char *nlerase(s)
    char *s;
    {
            register char *cp;

        cp = s;
        while (*cp != '\n' && *cp != '\0')
                ++cp;
        *cp = '\0';

        return (s);
}


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║ Click to view the listing found on page ║
                ║ 417 in the printed version of the book.  ║
                ║                                          ║
                ╚══════════════════════════════════════════╝


        Figure 14-4 shows VF in operation.


ViewFile/1.0  H=Help Q=Quit Esc=Next
File: vf.c (126 lines)
/*
    *      vf -- view a file using a full-screen window onto
    *      ┌──────────────────────────────────────────────────────────┐
    */     │ PgUp (U)        Scroll up the file one screen page       │
        │ PgDn (D)        Scroll down the file one screen page     │
#include│ Up arrow (-)    Scroll up in the file one line           │
#include│ Down arrow (+)  Scroll down in the file one line         │
#include│ Right arrow (>) Scroll right by 20 columns               │
#include│ Left arrow (<)  Scroll left by 20 columns                │
#include│ Home (B)        Go to beginning of file buffer           │
#include│ End (E)         Go to end of file buffer                 │
        │ Alt-g (G)       Go to a specified line in the buffer     │
extern i│ Alt-h (H or ?)  Display this help frame                  │
int Star│ Alt-n (N)       Toggle line-numbering feature            │
unsigned│ \ (R)           Reverse search for a literal text string │
unsigned│ / (S)           Search forward for a literal text string │
unsigned│ Esc             Next file from list (quits if none)      │
        │ Alt-q (Q)       Quit                                     │
main(arg│ -------------------------------------------------------- │
int argc│             << Press a key to continue >>                │
char **a└──────────────────────────────────────────────────────────┘
{
        int ch;

        FIGURE 14-4 ▐ The ViewFile program with the help frame displayed


Considerations and Alternatives

        If you need to view very large files, the use of external storage is
the best alternative. A theoretical maximum file size of 32 megabytes can
be achieved under current versions of DOS, but some of that space will
probably be needed to manage the data storage.
        With external storage, a file would be loaded from disk faster than by
the current VF because only a small piece of the file need be in memory at
one time. However, it's likely that there would be a significant increase
in the time needed to access a particular line in the file because the disk
must be accessed and read for each new portion of the file that is to be
displayed.
        An area of the VF design that could use some enhancement is the search
feature. Although literal-string searches satisfy most positioning
requirements, a regular-expression capability can be extremely handy.
Regular expressions are, in essence, text formulas that can be used to form
patterns rather than fixed strings.
        Another option to consider is an incremental search. As the user types
characters, the program accumulates them in the search pattern and moves
immediately to a string in the text, if there is one, that matches the
accumulated search string. The advantage of the incremental search is that
the user need only type enough characters to get the match. You would need
to provide an "escape" command to terminate the search when the desired
string is found.
        The implementation of VF presented in this chapter wraps around the
buffer boundaries. In most cases this action is appropriate, but not
always. It may be useful to have a "no wrap" option that would terminate
the search at the end (or beginning) of the buffer to avoid unwanted
repositioning.



SECTION V  Appendixes



Appendix A  Microsoft C Compiler, Version 4.00



        The current version of the Microsoft C Compiler at the time of this
writing is version 4.00. The compiler is noted for producing very fast and
compact executable program files. It trades compile time for a high degree
of optimization and produces object code that is arguably in the same
league as that produced by expert assembly-language programmers. "Hand
tuning" to improve performance is virtually unnecessary.


Technical Highlights

        The following material presents a general overview of the Microsoft C
Compiler, version 4.00. The compiler system encompasses a three-pass
compiler, extensive runtime library support, and a set of development
tools. See Chapter 1 for additional information about the compiler, the
impact of the proposed ANSI standard for C, and information about DOS and C
compiler programs that assist programmers in the development of
programs.
        Two compiler drivers are available with the Microsoft C Compiler. The
MSC driver executes the preprocessor and compiler passes. The CL driver is
similar to the CC driver on XENIX and UNIX, which compiles and links
programs automatically. Either driver can take advantage of a variety of
compile-time options.


        Program Size

        The Microsoft C Compiler now provides five distinct memory models--
small, medium, compact, large, and huge--plus extensions to produce mixed
models.

        ►  The small model generates tight, efficient code for programs with
        up to 64 KB of code and up to 64 KB of data.

        ►  The medium model supports programs with up to 1 MB of code but only
        64 KB of data.

        ►  The compact model handles programs with up to 64 KB of code and up
        to 1 MB of data.

        ►  The large model extends program space to 1 MB of code with 1 MB of
        data.

        ►  The huge model is similar to the large model, but allows individual
        arrays to be larger than 64 KB.

        Mixed memory models are made possible through the use of near, far,
and huge pointers. These pointers change the addressing conventions for one
or more elements without changing them for the program as a whole.


        Overlays

        The Microsoft C Compiler single-level overlay linker allows memory
space to be shared in turn by several program modules. Overlays may be
called from the "root" program or from another overlay and are
automatically loaded when needed.


        Multiple Math Libraries

        The Microsoft C Compiler offers several options for floating-point
operations. If an 8087/80287 math coprocessor is available, the smallest
and fastest floating-point library can be used. And even though a math
coprocessor may not be present, identical math results can still be
obtained from an emulator.
        This versatility gives programs the same high degree of precision
whether or not a math coprocessor is available. For maximum speed, the
emulator routines automatically use the math coprocessor if it is
installed.
        When full 80-bit precision and consistency with the 8087/80287 math
coprocessor are not required, the alternate math package can be selected.
Supporting a compatible interface for single- and double-precision IEEE
numbers, this math library is optimized for speed.


        Network File Sharing

        Under MS-DOS version 3.1 or higher with Microsoft Networks version
1.0, or under IBM PC Network, the Microsoft C Compiler supports multi-user
network access with both file and record locking.


        Direct Interlanguage Calls

        Routines written in the Microsoft C Compiler (version 3.00 or higher),
Microsoft FORTRAN (version 3.30 or higher), Microsoft Pascal (version 3.30
or higher), or Microsoft Macro Assembler can be linked together. So you can
share the investment you've made in developing libraries in a different
language.
        This interlanguage calling lets you use the best features of each
language, or upgrade to a new language while retaining libraries written in
the old one. Selected routines can be recoded as needed.


        Library Manager

        Entire program-development libraries can be built in any mix of C,
Pascal, FORTRAN, and assembly language by using the library manager that's
included with the Microsoft C Compiler.
        Once a library is constructed, the library subroutines can be called
directly from your C program. Subroutine calls are resolved when the
finished program is linked.


        Utility Programs

        The Microsoft C Compiler includes several utility programs; two are
used to increase the efficiency of .EXE files:

        ►  EXEPACK removes null characters from .EXE files and optimizes the
        relocation table. This results in smaller files on disk and faster
        loading at execution time.

        ►  EXEMOD allows fine-tuning of file header information usually set by
        default, such as stack size.


Language Details

        This is a summary of data types, keywords, compiler options,
preprocessor directives, and other important aspects of the Microsoft C
Compiler. Implementation-dependent items are grouped separately. Items that
are new in version 4.00 are flagged, as are library routines that are
implemented as macros.

═══════════════════════════════════════════════════════════════════════════
KEYWORDS
───────────────────────────────────────────────────────────────────────────
auto           else           long            switch
break          enum           register        typedef
case           extern         return          union
char           float          short           unsigned
continue       for            signed          void
default        goto           sizeof          while
do             if             static
double         int            struct

The keywords const and volatile are reserved for future use.


═══════════════════════════════════════════════════════════════════════════
IMPLEMENTATION-DEPENDENT KEYWORDS
___________________________________________________________________________
cdecl          fortran        near            pascal
far            huge


═══════════════════════════════════════════════════════════════════════════
FUNDAMENTAL TYPES
───────────────────────────────────────────────────────────────────────────
char           int            signed          void
double         long           unsigned
float          short

Enumeration types are also included.


═══════════════════════════════════════════════════════════════════════════
TYPE SPECIFIERS
───────────────────────────────────────────────────────────────────────────
char           int            struct          unsigned
double         long           typedef name    void
enum           short          union
float          signed


═══════════════════════════════════════════════════════════════════════════
DATA FORMATS
───────────────────────────────────────────────────────────────────────────
The standard C data types are implemented as follows:

Type                Length in Bytes     Range
───────────────────────────────────────────────────────────────────────────
char                1                   -128 to 127
double              8                   10**±306
float               4                   10**±38
int                 2                   -32768 to 32767
long                4                   -2**31 to 2**31-1
short               2                   -32768 to 32767
unsigned char       1                   0 to 255
unsigned int        2                   0 to 65535
unsigned long       4                   0 to 2**32-1
unsigned short      2                   0 to 65535


═══════════════════════════════════════════════════════════════════════════
POINTER SIZES
───────────────────────────────────────────────────────────────────────────
                    Bytes          Address Arithmetic
───────────────────────────────────────────────────────────────────────────
Near Pointer        2              16 bits (offset)
Far Pointer         4              16 bits (offset)
Huge Pointer        4              32 bits (segment offset)


═══════════════════════════════════════════════════════════════════════════
PREPROCESSOR DIRECTIVES
───────────────────────────────────────────────────────────────────────────
#define        #endif         #ifndef        #pragma
#elif          #if            #include       #undef
#else          #ifdef         #line


═══════════════════════════════════════════════════════════════════════════
STORAGE CLASSES
───────────────────────────────────────────────────────────────────────────
auto           extern         register       static


═══════════════════════════════════════════════════════════════════════════
EXTENDED TYPE MODIFIERS
───────────────────────────────────────────────────────────────────────────
cdecl          fortran        near           pascal
far            huge


═══════════════════════════════════════════════════════════════════════════
COMPILER STATEMENTS
───────────────────────────────────────────────────────────────────────────
break          default        for            return
case           do             goto           switch
continue       else           if             while


═══════════════════════════════════════════════════════════════════════════
COMPILER OPTIONS
───────────────────────────────────────────────────────────────────────────
The Microsoft C Compiler supports a large number of compile-time options.
Here is a partial list.

/A string          Sets program memory model
/Dname [=text]     Defines preprocessor macro
/Fx                Selects listing options for source, object, assembly,
                    and testing
/FPstring          Selects floating-point options
/Gx                Specifies options for code generation; generates
                    8086/8088, 80186/80188, or 80286 instructions;
                    disables stack checking
/I path            Adds to #include search path
/Ox                Controls optimization for speed and size
/Wn                Sets warning message level
/Zx                Selects language options for disabling extensions,
                    emitting debugging information, and structure packing


═══════════════════════════════════════════════════════════════════════════
LIBRARY FUNCTIONS
───────────────────────────────────────────────────────────────────────────
The Microsoft C Compiler contains an extensive set of UNIX System V
compatible library routines. This set includes many of the functions
included in the emerging ANSI standard. Functions new in this version
are designated by an exclamation point (!) before the function name.
Functions implemented as Macros are designated by a double exclamation
point (‼).

Buffer Manipulation
───────────────────────────────────────────────────────────────────────────
    memccpy        memcmp       !memicmp        movedata
    memchr         memcpy        memset

Character Classification and Conversion
───────────────────────────────────────────────────────────────────────────
‼isalnum       ‼isgraph      ‼isupper       ‼toupper
‼isalpha       ‼islower      ‼isxdigit      ‼_toupper
‼isascii       ‼isprint      ‼toascii
‼iscntrl       ‼ispunct      ‼tolower
‼isdigit       ‼isspace      ‼_tolower

Data Conversion
───────────────────────────────────────────────────────────────────────────
    atof           ecvt          itoa          !strtol
    atoi           fcvt          ltoa          ultoa
    atol           gcvt          !strtod

Directory Control
───────────────────────────────────────────────────────────────────────────
    chdir         getcwd         mkdir          rmdir

File Handling
───────────────────────────────────────────────────────────────────────────

    access        fstat          remove         unmask
    chmod         isatty         rename         unlink
    chsize        locking        setmode
    filelength    mktemp         stat

I/O Stream Routines
───────────────────────────────────────────────────────────────────────────
    clearerr      fopen         ‼getchar       !setvbuf
    fclose        fprintf        gets           sprintf
    fcloseall     fputc          getw           sscanf
    fdopen        fputchar       printf        !tempnam
‼feof          fputs         ‼putc          !tmpfile
‼ferror        fread         ‼putchar       !tmpnam
    fflush        freopen        puts           ungetc
    fgetc         fscanf         putw          !vfprintf
    fgetchar      fseek          rewind        !vprintf
    fgets         ftell         !rmtmp         !vsprintf
‼fileno        fwrite         scanf
    flushall     ‼getc           setbuf

Low-Level Routines
───────────────────────────────────────────────────────────────────────────
    close          dup2           open           tell
    creat          eof            read           write
    dup            lseek          sopen

Console and Port I/O Routines
───────────────────────────────────────────────────────────────────────────
    cgets          cscanf         inp            putch
    cprintf        getch          kbhit          ungetch
    cputs          getche         outp

Math (Floating Point)
───────────────────────────────────────────────────────────────────────────
    acos          !_control87     frexp          sin
    asin           cos            hypot          sinh
    atan           cosh           ldexp          sqrt
    atan2          exp            log           !_status87
    bessel         fabs           log10          tan
    cabs           floor          matherr        tanh
    ceil           fmod           modf
!_clear87      !_fpreset       pow

Memory Allocation
───────────────────────────────────────────────────────────────────────────
!alloca        !_fmsize        malloc        !_nmsize
    calloc         free          !_memavl        realloc
!_expand       !_freect       !_msize         sbrk
!_ffree        !halloc        !_nfree        !stackavail
!_fmalloc      !hfree         !_nmalloc

MS-DOS Interface
───────────────────────────────────────────────────────────────────────────
    bdos           int86          intdos         segread
    dosexterr      int86x         intdosx

Process Control
───────────────────────────────────────────────────────────────────────────
    abort          execve        !onexit         spawnv
    execl          execvp         signal         spawnve
    execle        !execvpe        spawnl         spawnvp
    execlp         exit           spawnle       !spawnvpe
!execlpe        _exit          spawnlp        system
    execv          getpid        !spawnlpe

Searching and Sorting
───────────────────────────────────────────────────────────────────────────
    bsearch      !lfind          !lsearch        qsort

String Manipulation
───────────────────────────────────────────────────────────────────────────
    strcat        strdup          strncmp        strrev
    strchr       !strerror        strncpy        strset
    strcmp       !stricmp        !strnicmp       strspn
    strcmpi       strlen          strnset       !strstr
    strcpy        strlwr          strpbrk        strtok
    strcspn       strncat         strrchr        strupr

Time
───────────────────────────────────────────────────────────────────────────
    asctime       ftime           localtime      tzset
    ctime         gmtime          time           utime
!difftime

Miscellaneous
───────────────────────────────────────────────────────────────────────────
‼abs           getenv          perror         setjmp
‼assert        labs            putenv         srand
‼FP_OFF        longjmp         rand           swab
‼FP_SEG
───────────────────────────────────────────────────────────────────────────



Appendix B  Other C Compilers



        There are many C compilers for DOS in today's PC marketplace. If you
are in the process of choosing one, I recommend that you read all you can
before making a purchase. Price is not the only consideration. Look for
features that will make your job easier: a complete standard library of
functions and macros; DOS and BIOS interface functions; program development
tools; and product support.
        If you are going to produce programs commercially, look for a compiler
that produces compact, fast-running programs. Ultimately, it is the user of
your programs who will benefit from your choice of a quality product. Using
a compiler that compiles faster than others but produces sloppy object code
is probably not in your best long-term interest if you intend to make a
living by writing programs. It may save you a little development time, but
it will penalize your program's users every time they run the
program.
        Interactive tools such as C interpreters are now available and provide
a comfortable testing and experimenting environment that helps a programmer
to quickly examine a variety of problem-solving approaches. You may want to
use a C interpreter for early development work. The current crop of C
interpreters are, however, less appropriate for large programming projects,
particularly projects that involve more than one programmer. Analyze your
needs carefully before selecting a C compiler or interpreter.
        Here are some magazine review articles that may help you decide which
C compiler is right for you:
        "The State of C," William Hunt, PC Tech Journal, January 1986.
        "Software Reviews (Department)--21 C Compilers," S. Leibson, J.
Reed, and J. Kyle, Computer Language, February 1985 (page 73).
        If you already have a C compiler and plan to stick with it, you have a
different concern. The task at hand is to use the available compiler
features and support tools to produce functioning programs. I have used
several C compilers in addition to the Microsoft C Compiler to compile the
programs presented in this book. They are the C86 C Compiler (Computer
Innovations), The C Programming System (Mark Williams Company), and the
MS-DOS C Compiler (Lattice). Each has its own way of doing things, although
nearly all C compiler vendors are starting to converge on the proposed ANSI
standard for C.
        Although it is possible to write source code with conditional
compilation directives to accommodate various compilers, I chose not to do
so in the source in Proficient C because that tends to obscure the meaning
of the code and makes the source files nearly unreadable. The following
material briefly describes each compiler system, the major observable
differences from Microsoft C, and what has to be done to accommodate those
differences. Others may exist. The ones mentioned are those relevant to the
programs and routines developed in Proficient C.
        The compilers mentioned here deliver full compatibility with the de
facto specification of C presented in the book The C Programming Language,
by Brian Kernighan and Dennis Ritchie, commonly referred to as "K&R" by the
C programming community. Each compiler varies from the others in the level
of support for the proposed ANSI standard for C.


Computer Innovations--Optimizing C86, Version 2.3

        One of the first C compiler entries into the PC marketplace was
Computer Innovations' C86, which was later replaced by Optimizing C86. The
compiler features good compatibility with the standard UNIX library (pre-
System V), support for many DOS features (all versions), object libraries
in MS-DOS object format, and is delivered with all library source (C and
assembler) in compressed form on disk.
        The compiler is due for an update at the time this is being written to
bring it up to System V and DOS (version 3.00 and higher) compatibility.
Optimizing C86 differs fundamentally from Microsoft C in that it:

        ►  Has no void function return specifier--use a typedef of an int to
        get around this.

        ►  Has no enum keyword--either avoid the use of enums or synthesize a
        palatable substitute using a typedef of an int.

        ►  Uses the sysint() function instead of intdos() and uses sysint21()
        instead of int86(). The calling sequence of parameters is
        different.

        ►  Provides movblock() instead of movedata() for segmented address
        memory moves. The parameter ordering is different.

        ►  Has no means of determining the DOS version provided by the
        compiler--use the ver() function described in Chapter 5.

        ►  Has no structure assignment and no passing of entire structures as
        parameters or function return values.

        ►  Has no perror()--use fprintf() and a message of your own making.

        Optimizing C86 supports small and large memory models in a way that is
consistent with the equivalent Microsoft models. Programs may also be
compiled in an 8080 .COM memory model, which is called "compact." It is not
the same as the Microsoft compact memory model. There are no equivalents to
the Microsoft medium and huge models. No data object can exceed 64 KB in
size.
        Compiling programs under C86 involves the use of batch files. There
are currently no compiler driver programs and no MAKE-like facility to
control processing. C86 can be given path information for the default
location of header files.


Mark Williams--The C Programming System, Version 3.0.7

        The Mark Williams C Programming System (MWC from now on) is a complete
C programming system adapted from the Mark Williams Coherent operating
system, a UNIX Version 7 work-alike system. MWC incorporates some of C's
newer features such as enum and void and up-to-date library functions.
        The compiler can be told to produce object files in either the Mark
Williams or Microsoft object file formats. Libraries for both formats are
provided with the compiler. MWC produces both small and large model (32-bit
pointers) programs. It offers nothing comparable to the compact, medium,
and huge Microsoft models.
        A public domain full-screen editor, MicroEMACS, is included in the
package along with a good selection of UNIX-style support tools (make, cc,
ld, pr, wc, and others).
        A trimmed-down version of MWC called Let's C is also available from
Mark Williams. It compiles small-model programs only and is supported by
fewer utilities, but it is a full K&R C compiler that is a good entry
vehicle for programmers who are new to C.
        The primary differences between MWC and Microsoft C are that
MWC:

        ►  Has no structure assignment and no passing of entire structures as
        parameters or function return values.

        ►  Uses intcall() instead of intdos(). It has no equivalent to the
        Microsoft int86(), but intcall() with an interrupt number of 21H
        will do the job (a bit slower, however).

        ►  Provides port access via inb() and outb() (equivalent to
        Microsoft's inp() and outp() routines).

        ►  Has no perror()--use fprintf() and a message of your own making.
        The global variable errno produces valid values in connection with
        math library functions only. It is not generally useful with other
        standard I/O library routines.


Lattice--MS-DOS C Compiler, Version 3.00H

        The Lattice C Compiler, particularly the library support package, has
undergone significant changes. The version 3.00 Lattice C compiler is  very
compatible with Microsoft C, versions 3.00 and 4.00; it differs mostly in
minor details:

        ►  The standard printer device is called stdprt instead of stdprn.

        ►  The Microsoft _osmajor and _osminor are handled by the external
        character variable _DOS, which may be accessed as a single
        character or a two-character array to obtain the DOS major and
        minor version numbers. You can use the ver() function described in
        the Proficient C DOS library to get the operating system version
        numbers.

        The MS-DOS interface functions emulate virtually all of the PC
operating system functions provided by Microsoft C, so calls to kbhit(),
int86(), intdos(), and so on require no modifications to compile under
Lattice C.
        The compiler is accompanied by a library that classifies each function
by its "environment" (i.e, origins and compatibility). The following
classes are defined by Lattice:

═══════════════════════════════════════════════════════════════════════════
CLASS          DESCRIPTION
───────────────────────────────────────────────────────────────────────────
ANSI           Conforming to the proposed ANSI standard
UNIX           Conforming to AT&T's UNIX System V standard
XENIX          Compatible with Microsoft XENIX
LATTICE        Available on all systems for which Lattice provides a
                C compiler
iAPX86         Available only on iAPX32 machines
MS-DOS         Designed for use under generic MS-DOS and compatible
                with PC-DOS (a superset of MS-DOS)
PC-DOS         For use under PC-DOS but not guaranteed to work under
                MS-DOS
───────────────────────────────────────────────────────────────────────────

        Lattice offers four memory models: small, program, data, and large.
The models are comparable to the Microsoft small, medium, compact, and
large models, respectively. Objects are limited to 64 KB in size. There are
no equivalents to Microsoft's huge keyword and huge model.
        Compiling programs with the latest Lattice C compiler is easier than
under previous versions. A compiler driver program, LC, does most of the
work of compiling by calling the passes in the correct order and bailing
out in the event of compile-time errors. The linking step is still manual
and is best handled by batch files tailored to the particular linking task
and the selected memory model.


Maintaining Programs

        For owners of C compilers that do not provide a MAKE command, several
alternatives are available. The first is to use DOS batch files to control
compilation and linking steps. The DOS ERRORLEVEL (program exit code) can
be used to abort processing when errors occur (if the compiler programs use
the error-return facility and you are running DOS version 2.00 or higher).
The problem with batch files is that the burden of knowing which program
modules have to be remade is left to you.
        Another alternative is to use a third-party MAKE utility. An excellent
MAKE program called PolyMake is available from Polytron. It is as UNIX-like
as any DOS MAKE I have seen and can be made to work with any C compiler via
rules placed in a control file. Because PolyMake is closer to the UNIX
model that I am familiar with, I prefer to use it rather than the MAKEs
provided with most C compilers (with the exception of the Mark Williams
MAKE program, which is excellent). Several other MAKE utilities for DOS are
available at reasonable cost. Note: The makefiles in Proficient C are
written for the Microsoft version of MAKE. They will probably need to be
rewritten to run successfully with other MAKE programs.



Appendix C  Characters and Attributes



        A character code is a numeric value that is used to represent a
character in computer memory and to control peripheral devices such as
terminals, the console display screen, and printers. We are interested in
two primary character-coding schemes: the ASCII character set, because it
is the standard to which most commercial terminal and computer equipment
designs adhere, and the IBM extended ASCII character set used by the IBM PC
and work-alike computers.
        In dealing with terminals and PC console display devices, numeric
codes are also used to control the appearance of a displayed character
(video attribute), to prevent specified screen regions from being updated
(protect), and even to make certain areas of the screen invisible to the
user (nondisplay). IBM PCs do not directly support protected fields,
although our programs can simulate the effect. On a PC, a nondisplay field
is one that carries a special video attribute which sets the foreground
color to the same value as the background color.
        This appendix is a detailed summary of the characters and attributes
available to programmers of the IBM PC family of computers.


ASCII Character Codes

        The ASCII (American National Standard Code for Information
Interchange) character set is defined as a table of 7-bit codes that
represent a collection of control characters and printable characters. In a
7-bit environment all data bits are significant. In the 8-bit environment
typical of the IBM PC and similar equipment, the lower seven bits (0
through 6) are used to represent ASCII characters. The high bit (bit 7) is
used for IBM code extensions, which are discussed later in this appendix.
        Figure C-1 shows the relationship between various character
codes and the data bytes that hold them.


Bit #      7      6      5      4      3      2      1      0
        ┌──────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┐
        │▒▒▒▒▒▒│      │      │      │      │      │      │      │ Character
        │▒▒▒▒▒▒│      │      │      │      │      │      │      │ Byte
        └──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┘
Weight    128     64     32     16     8      4      2      1
        ▓▓▓▓▓▓▓  ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
            │                           │
        Code Exten-                 7-Bit ASCII Codes
        sion Bit

        ╔═══════════════════════════════════════════════════════╗
        ║   Character Code Ranges:                              ║
        ║                                                       ║
        ║          0-31    Control Characters       ▓           ║
        ║         32-126   Graphic Characters       ▓►  ASCII   ║
        ║            127   Delete Character         ▓           ║
        ║        128-255   IBM Special Graphic Characters       ║
        ╚═══════════════════════════════════════════════════════╝

        FIGURE C-1 ▐ Character codes and bytes


        ASCII Control Character Table

        ASCII codes 0 through 31 and 127 decimal (shown in Figure C-2,
next page) are called control codes because they are used to start,
stop, or modify some action. Several of the control characters have
meanings that vary with the context of their use, but most can be
categorized as being one or more of the following types:

        ►  Format Effector (FE)--controls the printed or displayed layout of
        graphic information

        ►  Communications Control (CC)--controls the operation of
        communication devices and networks

        ►  Information Separator (IS)_controls the logical separation of
        information

        It may seem a bit odd to have one control code (DEL = 127) isolated
from all the others. This is due to its earliest use with paper-tape
punches for which the DEL code means "erase or obliterate" an unwanted
character, which on a medium like paper tape could be done only by punching
out all seven of the holes (hence code 127 or 7F hex).
        In its design of the PC, IBM has attached special meanings to most of
the ASCII control codes so that they have displayable graphic content when
placed directly into display memory. Thus, the format effector CR (code 13
decimal), in addition to its "carriage return" meaning when embedded in
data streams, can also be used to place a musical-note symbol on the
display screen. Each of the control codes except NUL (0) have an associated
displayable symbol on the IBM PC.

╓┌─────┌─────────────────────────┌──────────┌─────────┌──────────┌───────────╖
NAME  DESCRIPTION               DEC        HEX       IBM        KEY
        (TYPE)                    CODE       CODE      GRAPHIC
───────────────────────────────────────────────────────────────────────────
NUL   Null                       0         00                   nothing
SOH   Start of heading           1         01        ☺          ^A
STX   Start of text              2         02        ☻          ^B
ETX   End of text                3         03        ♥          ^C
EOT   End of transmission        4         04        ♦          ^D
ENQ   Enquiry                    5         05        ♣          ^E
ACK   Acknowledge                6         06        ♠          ^F
BEL   Bell                       7         07        •          ^G
BS    Backspace                  8         08        ◘          ^H
HT    Horizontal tab             9         09                   ^I
LF☼1  Linefeed                  10         0A                   ^J
NAME  DESCRIPTION               DEC        HEX       IBM        KEY
        (TYPE)                    CODE       CODE      GRAPHIC
LF☼1  Linefeed                  10         0A                   ^J
VT    Vertical tab              11         0B                   ^K
FF    Formfeed                  12         0C                   ^L
CR    Carriage return           13         0D                   ^M
SO    Shift out                 14         0E        ♫          ^N
SI    Shift in                  15         0F        ☼          ^O
DLE   Data link escape          16         10        ►          ^P
DC1   Device control 1          17         11        ◄          ^Q
DC2   Device control 2          18         12        ↕          ^R
DC3   Device control 3          19         13        ‼          ^S
DC4   Device control 4          20         14        ¶          ^T
NAK   Negative acknowledge      21         15        §          ^U
SYN   Synchronous idle          22         16        ▬          ^V
ETB   End transmission block    23         17        ↨          ^W
CAN   Cancel                    24         18        ↑          ^X
EM    End of medium             25         19        ↓          ^Y
SUB   Substitute                26         1A        →          ^Z
ESC   Escape                    27         1B        ←          ^[
FS    File separator            28         1C        ∟          ^\
NAME  DESCRIPTION               DEC        HEX       IBM        KEY
        (TYPE)                    CODE       CODE      GRAPHIC
FS    File separator            28         1C        ∟          ^\
GS    Group separator           29         1D        ↔          ^]
RS    Record separator          30         1E                  ^^
US    Unit separator            31         1F                   ^_
DEL   Delete                   127         7F                  Del

        FIGURE C-2 ▐ ASCII control character table


        Printable Character Table

        The ASCII codes in the range of 32 to 126 decimal are designated
"graphic" characters to show that they have a visible representation on a
display device. Only the meaning of code 32, space (SP), which prints or
displays as a blank location in a graphic sequence, is defined by the ASCII
standard. All of the remaining graphic characters have meanings that are,
in effect, enforced by consensus. We simply accept the fact that code 65
decimal represents the letter 'A', for example.
        Nothing in the standard demands that the graphic symbols be shown as
Gothic or Roman or any other type style. That decision is left to the
terminal/computer maker and usually is determined by whatever type style
the producer of the video controller chip supplies. The following table
(Figure C-3) shows the accepted definitions of the graphic characters.

╓┌──────────┌────────┌───────────────────────────────────────────────────────╖
ASCII      DEC      HEX
──────────────────────
<space>    32       20
!          33       21
"          34       22
#          35       23
$          36       24
%          37       25
&          38       26
'          39       27
(          40       28
)          41       29
*          42       2A
+          43       2B
,          44       2C
ASCII      DEC      HEX
,          44       2C
-          45       2D
.          46       2E
/          47       2F
0          48       30
1          49       31
2          50       32
3          51       33
4          52       34
5          53       35
6          54       36
7          55       37
8          56       38
9          57       39
:          58       3A
;          59       3B
<          60       3C
=          61       3D
>          62       3E
?          63       3F
ASCII      DEC      HEX
?          63       3F
@          64       40
A          65       41
B          66       42
C          67       43
D          68       44
E          69       45
F          70       46
G          71       47
H          72       48
I          73       49
J          74       4A
K          75       4B
L          76       4C
M          77       4D
N          78       4E
O          79       4F
P          80       50
Q          81       51
R          82       52
ASCII      DEC      HEX
R          82       52
S          83       53
T          84       54
U          85       55
V          86       56
W          87       57
X          88       58
Y          89       59
Z          90       5A
[          91       5B
\          92       5C
]          93       5D
^          94       5E
_          95       5F
`          96       60
a          97       61
b          98       62
c          99       63
d         100       64
e         101       65
ASCII      DEC      HEX
e         101       65
f         102       66
g         103       67
h         104       68
i         105       69
j         106       6A
k         107       6B
l         108       6C
m         109       6D
n         110       6E
o         111       6F
p         112       70
q         113       71
r         114       72
s         115       73
t         116       74
u         117       75
v         118       76
w         119       77
x         120       78
ASCII      DEC      HEX
x         120       78
y         121       79
z         122       7A
{         123       7B
|         124       7C
}         125       7D
~         126       7E

        FIGURE C-3 ▐ The ASCII standard character set


IBM Extended ASCII Codes

        Because the ASCII character set is based on a 7-bit code, the high bit
of each byte used by the IBM PC is available for other uses. When bit 7
(the high bit) is a logical 1, the character codes range from 128 to 255
decimal (Figure C-4). IBM uses these extra 128 codes for special characters
(international symbols, line-and-block drawing characters, special symbols
for mathematics, and so forth).

╓┌──────────┌─────────┌──────────────────────────────────────────────────────╖
IBM
GRAPHIC    DEC      HEX
───────────────────────
Ç          128       80
ü          129       81
é          130       82
â          131       83
ä          132       84
à          133       85
å          134       86
ç          135       87
ê          136       88
ë          137       89
è          138       8A
ï          139       8B
î          140       8C
ì          141       8D
Ä          142       8E
Å          143       8F
É          144       90
IBM
É          144       90
æ          145       91
Æ          146       92
ô          147       93
ö          148       94
ò          149       95
û          150       96
ù          151       97
ÿ          151       98
Ö          152       99
Ü          154       9A
¢          155       9B
£          156       9C
¥          157       9D
₧          158       9E
ƒ          159       9F
á          160       A0
í          161       A1
ó          162       A2
ú          163       A3
IBM
ú          163       A3
ñ          164       A4
Ñ          165       A5
ª          166       A6
º          167       A7
¿          168       A8
⌐          169       A9
¬          170       AA
½          171       AB
¼          172       AC
¡          173       AD
«          174       AE
»          175       AF
░          176       B0
▒          177       B1
▓          178       B2
│          179       B3
┤          180       B4
╡          181       B5
╢          182       B6
IBM
╢          182       B6
╖          183       B7
╕          184       B8
╣          185       B9
║          186       BA
╗          187       BB
╝          188       BC
╜          189       BD
╛          190       BE
┐          191       BF
└          192       C0
┴          193       C1
┬          194       C2
├          195       C3
─          196       C4
┼          197       C5
╞          198       C6
╟          199       C7
╚          200       C8
╔          201       C9
IBM
╔          201       C9
╩          202       CA
╦          203       CB
╠          204       CC
═          205       CD
╬          206       CE
╧          207       CF
╨          208       D0
╤          209       D1
╥          210       D2
╙          211       D3
╘          212       D4
╒          213       D5
╓          214       D6
╫          215       D7
╪          216       D8
┘          217       D9
┌          218       DA
█          219       DB
▄          220       DC
IBM
▄          220       DC
▌          221       DD
▐          222       DE
▀          223       DF
α          224       E0
ß          225       E1
Γ          226       E2
π          227       E3
Σ          228       E4
σ          229       E5
µ          230       E6
τ          231       E7
Φ          232       E8
Θ          233       E9
Ω          234       EA
δ          235       EB
∞          236       EC
φ          237       ED
ε          238       EE
∩          239       EF
IBM
∩          239       EF
≡          240       F0
±          241       F1
≥          242       F2
≤          243       F3
⌠          244       F4
⌡          245       F5
÷          246       F6
≈          247       F7
°          248       F8
∙          249       F9
·          250       FA
√          251       FB
ⁿ          252       FC
²          253       FD
■          254       FE
           255       FF

        FIGURE C-4 ▐ The IBM extended ASCII character set


        Line-Drawing Characters_Quick Reference

        Among the IBM extended ASCII characters are some line-drawing
characters that allow us to draw boxes and other shapes that can be formed
from various corners and straight-line segments. The codes are a bit
scattered, so to make them accessible, Figure C-5 visually groups the
codes for single- and double-line drawing characters. The origin of
these design aids can be traced to Rich Schinnell in an item published
in PC World (November 1983).


    205    186                         196    186
═══════   ║                        ───────   ║
            ║                                  ║

    201    203    187                  214    210    183
╔═════  ══╦══  ═════╗              ╓─────  ──╥──  ─────╖
║         ║         ║              ║         ║         ║

    204    206    185                  199    215    182
║         ║         ║              ║         ║         ║
╠════  ═══╬═══  ════╣              ╟────  ───╫───  ────╢
║         ║         ║              ║         ║         ║

    200    202    188                  211    208    189
║         ║         ║              ║         ║         ║
╚═════  ══╩══  ═════╝              ╙─────  ──╨──  ─────╜


    196    179                         205    179
───────   │                        ═══════   │
            │                                  │

    218    194    191                  213    209    184
┌─────  ──┬──  ─────┐              ╒═════  ══╤══  ═════╕
│         │         │              │         │         │

    195    197    180                  198    216    181
│         │         │              │         │         │
├────  ───┼───  ────┤              ╞════  ═══╪═══  ════╡
│         │         │              │         │         │

192      193    217                  212    207    190
│         │         │              │         │         │
└─────  ──┴──  ─────┘              ╘═════  ══╧══  ═════╛

        FIGURE C-5 ▐ Line-drawing character sets


        Block Characters--Quick Reference

        The treasure trove of special characters in the IBM extended ASCII
character set also contains eight block characters that can be used to good
effect in screen displays. They are detailed in Figure C-6.


                ╔══════════════════════════════════════════╗
                ║                                          ║
                ║    Figure C-6 is found on page 445       ║
                ║    in the printed version of the book.   ║
                ║                                          ║
                ╚══════════════════════════════════════════╝

        FIGURE C-6 ▐ Block character sets


Video Attributes

        Both the monochrome and color display systems used with PCs give us a
considerable degree of control over the appearance of characters on the
screen. Each character byte in memory is accompanied by an attribute byte
that specifies the visual characteristics of each displayed
character.
        Figure C-7 shows how the attribute byte is
interpreted.


Bit #      7      6      5      4      3      2      1      0
        ┌──────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┐
        │  BL  │  R   │  G   │  B   │  I   │  R   │  G   │  B   │ Attribute
        └──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┘ Byte
        ░ 128     64     32     16     8      4      2      1
Weight ◄░
        ░  8       4      2      1     8      4      2      1
            ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓  ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
                        │                           │
                    Background                   Foreground

        ╔═══════════════════════════════════════════════════════╗
        ║   Bit designations:                                   ║
        ║                                                       ║
        ║      BL  Foreground blink or background intensity     ║
        ║       I  Foreground intensity                         ║
        ║       R  Red                                          ║
        ║       G  Green                                        ║
        ║       B  Blue                                         ║
        ╚═══════════════════════════════════════════════════════╝

        FIGURE C-7 ▐ Video attribute byte interpretation


        The following table summarizes the text mode attributes for both
monochrome and color/graphics systems. The codes between 0 and 7 set the
foreground (low 4 bits) or background (high 4 bits) attribute. Thus,
storing the code 0x02 in an attribute byte produces a green foreground and
a black background.
        The high bit in each 4-bit (nibble) attribute component determines the
intensity. In the foreground component, setting bit 3 causes the foreground
to be high intensity. Setting bit 7 of an attribute byte controls either
foreground blinking or background intensity. On a CGA, port 3D8, bit 5
enables foreground blinking when set and disables blinking when cleared.
The power-on default is blink (bit 5=1). The equivalent MDA port is 3B8.
        We can combine attribute values using bit shifting and ORing to
produce various compound attributes. The combined code

        (0x01 << 4) | 0x07 | 0x08

yields a bright white character on a blue background.

═══════════════════════════════════════════════════════════════════════════
PRIMARY ATTRIBUTES
───────────────────────────────────────────────────────────────────────────
Dec     Hex      Binary      Attribute  Description
Code    Code     Code        CGA        MDA FG             MDA BG
───────────────────────────────────────────────────────────────────────────
0       00       0000        Black      Black              Black
1       01       0001        Blue       White Underline    White
2       02       0010        Green      White              White
3       03       0011        Cyan       White              White
4       04       0100        Red        White              White
5       05       0101        Magenta    White              White
6       06       0110        Brown      White              White
7       07       0111        White      White              White
───────────────────────────────────────────────────────────────────────────


═══════════════════════════════════════════════════════════════════════════
INTENSITY MODIFIERS
───────────────────────────────────────────────────────────────────────────

Dec     Hex      Binary       Description
Code    Code     Code
───────────────────────────────────────────────────────────────────────────
8       08       1000         High Intensity
128     80       10000000     Blinking Foreground (or High Intensity
                                Background). [This is just (0x08 << 4) and
                                is referred to as the "blink" bit or
                                background intensity bit depending upon
                                whether blinking is enabled (default) or
                                disabled.]
───────────────────────────────────────────────────────────────────────────



Appendix D  Local Library Summary



        Proficient C is largely about stockpiling routines to augment those
provided in the standard library. In the course of 14 chapters, we have
developed numerous functions and macros for a wide range of purposes. To
make the routines accessible to programmers, a brief manual page for each
routine is presented here.
        The routines are grouped by library category and organized
alphabetically within categories. The libraries we developed are the
ansi.sys interface (ansi.lib), the operating system interfaces (bios.lib
and dos.lib), the buffered screen interface (sbuf.lib), and the utility
routines (util.lib).

═══════════════════════════════════════════════════════════════════════════
ANSI LIBRARY
───────────────────────────────────────────────────────────────────────────
The ANSI device driver is accessed primarily through the macros defined in
the ansi.h header file that resides in \include\local. Two additional
routines are implemented as functions in \lib\local\ansi.lib. The ANSI
device driver and this interface are described in Chapter 13.

───────────────────────────────────────────────────────────────────────────
Function
        ansi_cpr--cursor position report

Synopsis

            #include <local\ansi.h>

            void ansi_cpr(row, col)
            int *row;       /* pointer to row value */
            int *col;       /* pointer to column value */

Description
        Request a cursor position report from the ANSI device driver. The
        driver must have been previously installed. The cursor position values
        are saved in the row and col variables whose addresses are passed as
        parameters.

Notes
        This function will fail (hang the system waiting for keyboard input)
        if the ANSI device driver is not properly installed.

───────────────────────────────────────────────────────────────────────────
Macro
        ANSI_CUP--position the cursor
        ANSI_HVP--horizontal and vertical position

Synopsis

            #include <local\ansi.h>

            ANSI_CUP(r, c)
            ANSI_HVP(r, c)
            short r, c;     /* row and column */

Description
        These two macros achieve the same result. They attempt to move the
        cursor to the specified screen position. Illegal requests are ignored.

───────────────────────────────────────────────────────────────────────────
Macro
        ANSI_CUB--move cursor back (left)
        ANSI_CUD--move cursor down
        ANSI_CUF--move cursor forward (right)
        ANSI_CUU--move cursor up

Synopsis

            #include <local\ansi.h>

            ANSI_CUB(n)
            ANSI_CUD(n)
            ANSI_CUF(n)
            ANSI_CUU(n)
            short n;        /* number of repetitions (columns or rows) */

Description
        These four macros attempt to move the cursor in the specified
        direction. If n is larger than the number of rows or columns remaining
        in the direction of travel, the cursor is moved to the extreme
        position.

───────────────────────────────────────────────────────────────────────────
Macro
        ANSI_DSR--request a device status report
        ANSI_RCP--restore screen position
        ANSI_SCP--save screen position

Synopsis

            #include <local\ansi.h>

            ANSI_DSR
            ANSI_RCP
            ANSI_SCP

Description
        A call to ANSI_DSR causes the ANSI driver to deposit a string
        containing the cursor row and column data into the keyboard buffer
        where it can be read (see the ansi_cpr() function). ANSI_SCP and
        ANSI_RCP are cooperating macros that communicate through private
        storage to save and restore the cursor position.

───────────────────────────────────────────────────────────────────────────
Macro
        ANSI_ED--erase in display (clear screen)
        ANSI_EL--erase in line (clear to end of line)

Synopsis

            #include <local\ansi.h>

            ANSI_ED
            ANSI_EL

Description
        ANSI_ED sets all character locations in the display to blanks,
        effectively clearing the screen. Similarly, ANSI_EL sets all
        character positions from the cursor to the end of the line to blanks.

Notes
        Most ANSI drivers use the prevailing video attribute for cleared
        character positions, but at least the MS-DOS version of the ANSI
        driver provided for the AT&T 6300 uses the "normal" (white on black)
        attribute.

───────────────────────────────────────────────────────────────────────────
Macro
        ANSI_RM--reset mode
        ANSI_SM--set mode

Synopsis

            #include <local\ansi.h>

            ANSI_RM(m)
            ANSI_SM(m)
            short m;

Description
        These two macros are used to set and reset (clear) video modes. In
        addition to the usual video modes that can be set from the DOS command
        line using the MODE command, ANSI_SM and ANSI_RM permit programs to
        control "wrapping" at the end of display lines.

───────────────────────────────────────────────────────────────────────────
Macro
        ANSI_SGR--set graphic rendition (video attribute)

Synopsis

            #include <local\ansi.h>

            ANSI_SGR(a)
            unsigned char a;        /* video attribute */

Description
        ANSI_SGR can be used to set any of the video attributes allowed for
        text modes.

───────────────────────────────────────────────────────────────────────────
Function
        ansi_tst--check for the presence of ANSI.SYS

Synopsis

            #include <local\ansi.h>

            void ansi_tst()

Description
        Uses cursor positioning and reading to determine whether the ANSI
        device driver is installed. If not, the function displays an error
        message and returns control to DOS. If the ANSI driver is installed,
        control is returned to the calling program.

Notes
        This function uses the BIOS readca() function to find out where the
        cursor is located because ansi_cpr() cannot be called unless the ANSI
        driver is known to be installed. Thus, ansi_tst() may fail on PCs that
        are not completely BIOS-compatible with the IBM PC family.

───────────────────────────────────────────────────────────────────────────
Function
        colornum--convert a color string to a number

Synopsis

            #include <local\ibmcolor.h>

            int colornum(name)
            char *name;             /* color string */

Description
        Using a built-in lookup table, colornum() converts a color name in
        string form, "red" for example, to its IBM numeric equivalent (4 in
        this case).

Return
        A number that specifies the IBM color value for the specified color
        name.

───────────────────────────────────────────────────────────────────────────
Function
        setattr--set video attribute

Synopsis

            #include <local\ansi.h>
            #include <local\ibmcolor.h>

            void setattr(pos, attr)
            POSITION pos;
            int attr;

Description
        Using information provided by the caller about the attribute position
        (pos), setattr() sets the foreground, background, or border attribute
        to the specified value.

───────────────────────────────────────────────────────────────────────────
Function
        userattr--set a user-specified attribute

Synopsis

            #include <local\ansi.h>

            int userattr(foreground, background, border)
            char *foreground, *background, *border;

Description
        A program can call userattr() just before exiting to set the video
        attributes to those specified by the user in the DOS environment
        (using FGND, BKGND, and BORDER DOS variables). The calling function
        must specify attribute values to be used in the event that none are
        specified in the environment.

Return
        An indication of SUCCESS or FAILURE.

═══════════════════════════════════════════════════════════════════════════
BIOS LIBRARY
───────────────────────────────────────────────────────────────────────────

The BIOS library is composed of selected routines supported by the PC's ROM
BIOS. Primary coverage is given to BIOS video and equipment-determination
functions. In addition, keyboard status and some composite video functions
are included in the library. Most other needed services can be obtained via
routines in the standard libraries provided with nearly all C compilers for
DOS.

Most of the BIOS routines are designed to return the value of the carry
flag, which is a nonzero value if an error occurs duing the interrupt
operation. A few use the return value to pass back the information
requested by the calling function.

───────────────────────────────────────────────────────────────────────────
Function
        clrscrn--clear the entire screen to spaces
        clrw--clear a "window" region to spaces

Synopsis

            int clrscrn(a)
            int clrw(t, l, b, r, a)
            unsigned char a;        /* video attribute */
            int t, l;               /* upper left corner */
            int b, r;               /* lower right corner */

Description
        The two functions, clrscrn() and clrw(), set all characters to spaces
        in the entire display memory or a specified region, respectively. The
        current "visual" page is cleared.

Return
        The value of the carry flag.

───────────────────────────────────────────────────────────────────────────
Function
        delay--provide a machine-independent time delay

Synopsis

            void delay(d)
            float d;                /* delay duration */

Description
        The delay() function loops to waste the specified number of seconds
        and fractional seconds. The delay has a practical resolution of about
        .06 seconds.

───────────────────────────────────────────────────────────────────────────
Function
        drawbox--draw a single-line text graphics box shape

Synopsis

            int drawbox(top, lft, btm, rgt, pg)
            int top, lft;           /* upper left corner */
            int btm, rgt;           /* lower right corner */
            int pg;                 /* screen page number */

Description
        The drawbox() function draws a fine-rule box (using the IBM single-
        line drawing characters) around the perimeter of the region specified
        by the corner parameters. The pg parameter specifies the screen page
        (active) to draw on. On a standard monochrome display adapter, this
        value must be 0.

Notes
        This function is usually not appropriate for use in graphics modes
        because the special drawing characters are not defined unless you
        create your own table of "extended" character definitions.

───────────────────────────────────────────────────────────────────────────
Function
        ega_info--gather EGA-related information

Synopsis

            #include <local\video.h>

            int ega_info(memsize, mode, features, switches)
            int *memsize;                   /* memory size index (0-3) */
            int *mode;                      /* color (0)/mono (1) mode */
            unsigned int *features;         /* feature bit settings */
            unsigned int *switches;         /* EGA switch settings */

Description
        This function uses an EGA BIOS routine to gather and save EGA-related
        information including whether an EGA adapter is installed in the
        system. Memory size is reported by an index (from 0 = 64 KB up to
        3 = 256 KB in 64-KB increments) and mode is indicated only as color
        (0) or monochrome. Use the getstate() function to find out what the
        video mode number is.

Return
        A logical 1 if the reported memory size is valid or 0 otherwise. This
        is considered by IBM to be an adequate indicator of the presence or
        absence of an EGA adapter.

───────────────────────────────────────────────────────────────────────────
Function
        equipchk--get equipment list

Synopsis

            #include <local\equip.h>

            int equipchk()

Description
        The equipchk() function queries the system data areas and fills in the
        global Eq structure to indicate what equipment is installed. The data
        structure holds the number of logical disk drives, parallel printer
        ports, and serial ports. It also indicates whether the system has a
        game port and shows how much of the installed memory is on the system
        board. In addition, the current video mode is indicated by the BIOS
        video mode number (see getstate() for details).

Return
        The value of the carry flag.

───────────────────────────────────────────────────────────────────────────
Function
        getctype--get the current cursor type

Synopsis

            #include <local\bioslib.h>

            int getctype(start_scan, end_scan, pg)
            int *start_scan;        /* starting cursor scan line */
            int *end_scan;          /* ending cursor scan line */
            int pg;                 /* "visual" page */

Description
        The getctype() function retrieves the values of the starting and
        ending cursor scan rows.

Return
        The value of the carry flag.

───────────────────────────────────────────────────────────────────────────

Function
        getstate--get the current video state

Synopsis

            #include <local\video.h>

            int getstate()

Description
        This function queries the system data areas in memory and updates the
        global video state variables.

Return
        The value of the carry flag.

───────────────────────────────────────────────────────────────────────────
Function
        getticks--get the current timer "tick" count

Synopsis

            long getticks()

Description
        This function queries the BIOS data area via the time-of-day service
        to get a value that is the number of timer ticks since midnight. If
        the rollover flag is set, a full day's worth of ticks is added to the
        count.

Return
        The current timer "tick" count.

───────────────────────────────────────────────────────────────────────────
Function
        kbd_stat--get keyboard status information

Synopsis

            #include <local\keybdlib.h>

            unsigned char kbd_stat()

Description
        The kbd_stat() function obtains keyboard status information and makes
        it available to the calling function. Each bit in the status byte is
        significant and is defined in the keybdlib.h file. This function lets
        the program determine the current state of the shift, control (Ctrl),
        alternate shift (Alt), and lock (Num, Caps, Scroll) keys.

Return
        The value of the AL register, which contains the keyboard status
        information (bit-significant).

───────────────────────────────────────────────────────────────────────────
Function
        memsize--get memory size in kilobytes

Synopsis

            int memsize()

Description
        The memsize() function reads the system data area to determine the
        total amount of memory installed in the system.

Return
        The total memory size (system board plus I/O channel) in kilobytes.

───────────────────────────────────────────────────────────────────────────
Function
        palette--select graphics palette or text border color

Synopsis

            int palette(id, color)
            unsigned int id;        /* palette identifier */
            unsigned int color;     /* color number */

Description
        The palette() function can be used in either graphics or text
        (alphanumeric) modes. It selects the palette of foreground drawing
        colors in graphics modes. In text modes, it selects the border color.

Return
        The value of the carry flag.

───────────────────────────────────────────────────────────────────────────
Function
        putcur--move the cursor to a specified row and column

Synopsis

            int putcur(r, c, pg)
            int r, c;               /* row and column */
            int pg;                 /* "active" screen page */

Description
        This function places the cursor at the specified row and column of the
        active page given by pg. Illegal requests are silently ignored by the
        BIOS routine.

Return
        The value of the carry flag.

───────────────────────────────────────────────────────────────────────────
Function
        putfld--display a string in a field

Synopsis

            int putfld(s, w, pg)
            register char *s;       /* string to write */
            int w;                  /* field width */
            int pg;                 /* screen page for writes */

Description
        The putfld() function calls other BIOS functions to display a string
        within a field starting on page pg at the current cursor position and
        extending for w columns. Runs of a single given character, such as a
        series of spaces, are compressed to a single call on writec() to
        reduce function-call overhead. The field is padded with spaces if the
        string does not completely fill it. The string is truncated if it is
        too long to fit in the field. The cursor location is updated to the
        last position written.

Return
        A 0 if all goes well or a nonzero value to flag an error.

───────────────────────────────────────────────────────────────────────────
Function
        putstr--display a string in the prevailing attribute

Synopsis

            int putstr(s, pg)
            register char *s;       /* string to display */
            int pg;                 /* "active" screen page */

Description
        The putstr() function places the text of the string into display
        memory and advances the cursor position.

Return
        Number of characters written.

Notes
        Results are undefined if the string is not confined to the current
        screen row.

───────────────────────────────────────────────────────────────────────────
Function
        put_ch--display a character

Synopsis

            int put_ch(ch, pg)
            register char ch;       /* character to write */
            int pg;                 /* "active" screen page */

Description
        The put_ch() function writes a single character to the display and
        advances the cursor position.

Return
        Always 1 to indicate the number of characters written.

───────────────────────────────────────────────────────────────────────────
Function
        readca--read the character and attribute in a cell

Synopsis

            int readca(ch, attr, pg)
            unsigned char *ch;              /* character */
            unsigned char *attr;            /* video attribute */
            int pg;                         /* "active" screen page */

Description
        The readca() function gets the values of the character and video
        attribute at the current cursor position on the specified active
        screen page and fills in the ch and attr variables pointed to by the
        parameters.

Return
        The value of the carry flag.

───────────────────────────────────────────────────────────────────────────
Function
        readcur--get the current cursor row and column

Synopsis

            int readcur(row, col, pg)
            int *row, *col;         /* pointers to cursor values */
            int pg;                 /* "active" screen page */

Description
        The readcur() function reports the location of the cursor on the
        specified screen page by storing the row and column values in the row
        and col variables pointed to by the parameters.

Return
        The value of the carry flag.

───────────────────────────────────────────────────────────────────────────
Function
        readdot--read the value of a single pixel

Synopsis

            int readdot(row, col, dcolor)
            int row, col;           /* cursor position */
            int *dcolor;            /* pointer to dot color */

Description
        The readdot() function places the color number for the pixel at the
        row and column position into the variable pointed to by the dcolor
        parameter. The row and col parameters can be used to address up to
        224,000 individual screen pixels in the high resolution mode of the
        Enhanced Graphics Adapter.

Return
        The value of the carry flag.

───────────────────────────────────────────────────────────────────────────
Function
        scroll--scroll a specified screen region vertically

Synopsis

            int scroll(t, l, b, r, n, a)
            int t, l;               /* upper left corner */
            int b, r;               /* lower right corner */
            int n;                  /* number of rows to scroll */
            unsigned char a;        /* video attribute */

Description
        The scroll() routine clears (initializes) the specified region if n is
        0. It scrolls the display image up n lines if n is a positive nonzero
        value and down if n is negative. Vacated lines are filled with blanks
        (spaces) in the specified attribute.

Return
        The value of the carry flag.

───────────────────────────────────────────────────────────────────────────
Function
        setctype--set the cursor type

Synopsis

            int setctype(start, end)
            int start;              /* starting cursor scan line */
            int end;                /* ending cursor scan line */

Description
        The setctype() function sets the starting and ending scan lines that
        form the cursor shape. If the starting scan line is greater than the
        ending scan line, the cursor is split (formed of two parts) on
        perfectly IBM-compatible machines. Setting the cursor starting scan
        line to a large value turns the cursor off on many machines, but this
        is not a reliable method on all hardware.

Return
        The value of the carry flag.

───────────────────────────────────────────────────────────────────────────
Function
        setpage--select the "visual" video page

Synopsis

            int setpage(pg)
            int pg;                 /* "visual" screen page */

Description
        This function sets the "visual" screen page to pg. It checks the value
        of pg against the valid page ranges for the current video mode.

Return
        The value of the carry flag or -1 if an illegal page request is made.

───────────────────────────────────────────────────────────────────────────
Function
        setvmode--set the video mode

Synopsis

            int setvmode(vmode)
            int vmode;              /* video mode number */

Description
        The setvmode() function attempts to set the specified video mode.
        Illegal mode requests are not detected and are usually silently
        ignored by the BIOS routine.

Return
        The value of the carry flag.

Notes
        This function cannot be used to switch from one display adapter to
        another in systems equipped with more than one adapter. Instruct users
        to run the DOS MODE command to set the mode before entering the
        program.

───────────────────────────────────────────────────────────────────────────
Function
        writea--write an attribute
        writec--write a character
        writeca--write a character in a specified attribute

Synopsis

            int writea(a, count, pg)
            int writec(ch, count, pg)
            int writeca(ch, attr, count, pg)
            unsigned char ch;       /* character */
            unsigned char attr;     /* video attribute */
            int count;              /* number of repetitions */
            int pg;                 /* "active" screen page */

Description
        These functions write a (possibly) repeated attribute, character, or a
        combination of the two to the specified screen page. The count
        specifies the number of characters to write and must fit entirely
        within the current screen row.

Return
        The value of the carry flag.

───────────────────────────────────────────────────────────────────────────
Function
        writedot--write a single pixel

Synopsis

            int writedot(r, c, color)
            int r, c;               /* coordinates of the pixel */
            int color;              /* dot color */

Description
        This function is for use in graphics modes only. It sets the color of
        the specified pixel to color. Setting the color to the same color as
        the background effectively erases the pixel.

Return
        The value of the carry flag.

───────────────────────────────────────────────────────────────────────────
Function
        writemsg--write a two-part message

Synopsis

            int writemsg(r, c, w, s1, s2, pg)
            int r, c;               /* starting location */
            int w;                  /* field width */
            char *s1, *s2;          /* components of the string */
            int pg;                 /* "active" screen page */

Description
        The writemsg() function places a two-part message (either or both
        parts may be empty) on the specified screen page. If the combined
        string is too long to fit in the field it is truncated. If it does not
        fill the field exactly, the field is padded with spaces.

Return
        The number of characters written.

───────────────────────────────────────────────────────────────────────────
Function
        writetty--write a character using "teletype" style

Synopsis

            int writetty(ch, attr, pg)
            unsigned char ch;       /* character */
            unsigned char attr;     /* video attribute */
            int pg;                 /* "active" screen page */

Description
        The writetty() function places a character into the specified screen
        page and advances the cursor position. The screen is scrolled if
        necessary to keep the current position within the viewable screen
        boundaries. The function responds to the usual format effectors
        (backspace, carriage return, and linefeed) correctly. All other codes,
        including other control codes, are simply displayed.

Return
        The value of the carry flag.

Notes
        The attribute specification is honored only in text modes. It is
        simply ignored in graphics modes.

═══════════════════════════════════════════════════════════════════════════
DOS LIBRARY
───────────────────────────────────────────────────────────────────────────
Only a very limited set of DOS routines are packaged in the local DOS
library because the standard library provides rather complete DOS access to
disk files, the console keyboard, DOS date and time, and so on.

───────────────────────────────────────────────────────────────────────────
Function
        drvpath--convert a drive name to a full pathname

Synopsis

            char *drvpath(path)
            char path[];            /* path string buffer */

Description
        A disk drive designation (d:) is a shorthand notation for the current
        directory on disk drive d. The drvpath() function accepts a disk drive
        designation as the initial portion of a character buffer and appends
        the full pathname of the current directory on that drive to it.

Return
        A pointer to a full pathname string starting with the drive letter.

Notes
        The user-supplied buffer (path) must be large enough to hold the
        longest DOS directory pathname (64 characters) plus a terminating NUL.

───────────────────────────────────────────────────────────────────────────
Function
        first_fm--find the first matching filename
        next_fm--find the next matching filename

Synopsis

            int first_fm(path, fa)
            int next_fm()
            char *path;             /* ambiguous pathname */
            int fa;                 /* file attributes */

Description
        Under DOS, we can specify filenames ambiguously using the * and ?
        wildcard characters. The first_fm() function finds the first match in
        directory order to an ambiguous specification. If the first match is
        found, additional matches, if any, may be found by calling the
        next_fm() function repeatedly until no match is found.

        The file attributes specified by the fa parameter determine which
        files that match the ambiguous name will be matched. If fa equals 0,
        the functions attempt to match only ordinary files. If the volume-
        label attribute is set, the functions search only for a volume label.
        Setting fa to system, hidden, subdirectory, or any combination of
        these attributes tells the functions to match files having those
        attributes in addition to all ordinary files.

Return
        Both functions return the value of the carry flag, which is 0 for
        success or nonzero for failure.

Notes
        These functions use a disk transfer area of type struct DTA, which is
        defined in the header file ls.h. The DTA is established by a call to
        setdta(). If any intervening functions alter the DTA, it must be reset
        by calling setdta() before making further calls to next_fm.

───────────────────────────────────────────────────────────────────────────
Function
        getdrive--return the ID of the default drive

Synopsis

            int getdrive()

Description
        The getdrive() function asks DOS for the ID of the current disk drive.
        The ID is the number used internally by DOS where 0 means drive A, 1
        means B, and so on.

Return
        The internal drive number of the current disk drive.


───────────────────────────────────────────────────────────────────────────
Function
        getkey--get a keystroke

Synopsis

            #include <local\keydefs.h>

            int getkey()

Description
        Gets the next available key code from the keyboard buffer. If nothing
        is ready, getkey() waits for the user to type something. If you do not
        want a "blocking" read, use keyready() (or kbhit()) to determine
        whether anything is ready to read before calling getkey().

Return
        The code associated with the oldest item in the keyboard buffer. If
        the returned value is 256 or more, the input was produced by a special
        key and the returned value represents an extended key code (NUL + scan
        code).

Notes
        Because getkey() uses DOS function 7 (INT 21H), it is immune to Ctrl-
        Break and it does not echo the typed input to the screen.

───────────────────────────────────────────────────────────────────────────
Function
        keyready--check the keyboard buffer

Synopsis

            int keyready()

Description
        Checks the console for a waiting keystroke.

Return
        A nonzero value if something is waiting in the keyboard buffer.

Notes
        This function performs the same job as the kbhit() function in the
        standard Microsoft library. It is presented for those whose C
        compilers offer no equivalent function.

───────────────────────────────────────────────────────────────────────────
Function
        setdta--set the disk transfer area (DTA)

Synopsis

            void setdta(bp)
            char *bp;               /* buffer pointer */

Description
        This function establishes a buffer in memory where disk transfers take
        place. There may be multiple DTAs of varying sizes but only one may be
        active at any time. If no DTA is established by a program, DOS sets up
        a 128-byte default DTA in the program segment prefix (PSP) of the
        running program.

───────────────────────────────────────────────────────────────────────────
Function
        ver--get the DOS major and minor version numbers

Synopsis

            int ver()

Description
        Gets the DOS version major and minor numbers.

Return
        The ver() function returns the DOS major version number in the low
        byte (bits 0-7) and the minor version number in the high byte (bits
        8-15).

Notes
        These values duplicate the values in the global _osmajor and _osminor
        variables supported by the Microsoft C Compiler. They are here for use
        with compilers that support the bdos() function but not the global
        version variables.

═══════════════════════════════════════════════════════════════════════════
SCREEN-BUFFER LIBRARY
───────────────────────────────────────────────────────────────────────────

The screen-buffer library routines are described in Chapter 12. The
routines are used to form a display image in an off-screen buffer and to
copy the image in the buffer to display memory.

───────────────────────────────────────────────────────────────────────────
Function
        sb_box--draw a box around a window

Synopsis

            #include <local\sbuf.h>
            #include <local\box.h>

            int sb_box(win, type, attr)
            struct REGION *win;             /* target window */
            short type;                     /* line drawing type */
            unsigned char attr;             /* video attribute */

Description
        Draws a box shape around a rectangular region (defined by win) in the
        screen buffer. Types are summarized in the following table:

═══════════════════════════════════════════════════════════════════════════
Type           Description
───────────────────────────────────────────────────────────────────────────
0              Default type: + for corners, | for vertical edges,
                and - for horizontal edges
1              Single lines all around
2              Double lines all around
3              Single horizontal and double vertical lines
4              Double horizontal and single vertical lines
5              Block shapes for all lines and corners
───────────────────────────────────────────────────────────────────────────

Return
        SB_OK

───────────────────────────────────────────────────────────────────────────
Function
        sb_fill--fill the scrolling region of a window
        sb_filla--fill with an attribute only
        sb_fillc--fill with a character only

Synopsis

            #include <local\sbuf.h>
            extern struct BUFFER Sbuf;
            extern union CELL Scrnbuf[SB_ROWS][SB_COLS];

            int sb_fill(win, ch, attr)
            int sb_filla()
            int sb_fillc()
            struct REGION *win;             /* target window */
            unsigned char ch;               /* character */
            unsigned char attr;             /* video attribute */

Description
        These three functions fill the scrolling region of a window (the
        entire window if not set explicitly). The sb_fill() function takes
        both a character and an attribute as parameters. The sb_filla()
        function sets all cells to a common video attribute while leaving the
        character in each cell undisturbed. And sb_fillc() sets all cells to a
        common character value while leaving the attribute in each cell
        undisturbed.

Return
        SB_OK

───────────────────────────────────────────────────────────────────────────
Function
        sb_init--initialize the buffered-screen interface

Synopsis

            int sb_init()

Description
        Sets up the Sbuf buffer control structure and initializes the flag
        bits and line variables. The SB_DIRECT bit is set to a logical 1 if
        the DOS variable UPDATEMODE is set to DIRECT.

Return
        SB_OK if the interface can be initialized and SB_ERR if not (due to a
        bad update mode specification).

───────────────────────────────────────────────────────────────────────────
Function
        sb_move--locate the cursor within a window

Synopsis

            #include <local\sbuf.h>
            extern struct BUFFER Sbuf;
            extern union CELL Scrnbuf[SB_ROWS][SB_COLS];

            int sb_move(win, r, c)
            struct REGION *win;             /* target window */
            register short r, c;            /* row and column */

Description
        The cursor is moved to the specified window-relative row and column
        (r, c). The appropriate data structures are updated to reflect the
        change.

Return
        SB_OK if the cursor location is valid for the window or SB_ERR if
        either r or c is an invalid value.

───────────────────────────────────────────────────────────────────────────
Function
        sb_new--establish a window (screen-buffer region)

Synopsis

            #include <local\sbuf.h>

            struct REGION *sb_new(top, left, height, width)
            int top;                /* top row */
            int left;               /* left column */
            int height;             /* total rows */
            int width;              /* total columns */

Description
        This function establishes a rectangular region of the screen buffer as
        a window, defined by the upper left corner position and the height and
        width values. The scrolling region is initially set to the window
        values, but may be altered by a call to sb_set_scrl().

Return
        A pointer to a newly allocated window structure or NULL if the window
        could not be established.

───────────────────────────────────────────────────────────────────────────
Function
        sb_putc--put a character into a window
        sb_puts--put a string into a window

Synopsis

            #include <local\sbuf.h>
            extern struct BUFFER Sbuf;
            extern union CELL Scrnbuf[SB_ROWS][SB_COLS];

            int sb_putc(win, ch)
            int sb_puts(win, s)
            struct REGION *win;     /* target window */
            char ch;
            unsigned char *s;       /* text string */

Description
        The sb_putc() function puts the specified character into the screen-
        buffer array at the current window position and advances the window
        "cursor." If the character is a newline, the remainder of the current
        window row is cleared and the cursor advances to the first position of
        the next row. If scrolling is enabled and is required to keep the
        cursor inside the window, sb_scrl() is called and the cursor lands in
        the first position of the last window row.

        A call to sb_puts is treated as a call to sb_putc for each character
        in the string. A tab is expanded to spaces using a series of calls to
        sb_putc(). Expansion is limited to the current window row.

Return
        SB_OK or SB_ERR if an error occurs.

───────────────────────────────────────────────────────────────────────────
Function
        sb_ra--read attribute
        sb_rc--read character
        sb_rca--read character and attribute

Synopsis

            #include <local\sbuf.h>
            extern struct BUFFER Sbuf;
            extern union CELL Scrnbuf[SB_ROWS][SB_COLS];

            unsigned char sb_ra(win)
            unsigned char sb_rc(win)
            unsigned short sb_rca(win)

            union REGION *win;              /* target window */

Description
        Each of these functions is passed a window pointer and window-relative
        row and column values of a cell, and returns information about the
        specified cell. The sb_ra() function obtains only the video attribute;
        sb_rc() obtains only the character value. However, sb_rca() gathers
        both the character and the video attribute of the specified cell.

Return
        The sb_ra() and sb_rc() functions return a byte that contains the
        attribute or character at the current window cursor position. A word
        containing both the character and attribute is returned by sb_rca().

───────────────────────────────────────────────────────────────────────────
Function
        sb_scrl--scroll a window region
        sb_set_scrl--change the defined scrolling region

Synopsis

            #include <local\sbuf.h>
            extern struct BUFFER Sbuf;
            extern union CELL Scrnbuf[SB_ROWS][SB_COLS];

            int sb_scrl(win, n)
            int sb_set_scrl(win, top, left, bottom, right)

            union REGION *win;              /* target window */
            short top, left;                /* upper left corner */
            short bottom, right;            /* lower right corner */
            short n;                        /* number of rows to scroll */

Description
        If n is greater than 0, sb_scrl() scrolls the specified window down by
        n rows. A negative n causes upward scrolling. Vacated lines are filled
        with spaces in the prevailing attribute. If n equals 0, the entire
        scrolling region is set to spaces.

        Scrolling occurs within a window's scrolling region. The sb_set_scrl()
        function defines the scrolling region, which may be any size up to the
        full window size.

Return
        SB_OK. If an illegal scrolling region is specified, SB_ERR is returned
        by sb_set_scrl().

───────────────────────────────────────────────────────────────────────────
Function
        sb_show--copy the screen buffer to display memory

Synopsis

            #include <local\sbuf.h>
            extern struct BUFFER Sbuf;
            extern union CELL Scrnbuf[SB_ROWS][SB_COLS];

            int sb_show(pg)
            short pg;

Description
        The screen-buffer array is copied to display memory by sb_show(),
        which uses either BIOS routines or direct display memory accesses,
        depending on the value of the variable UPDATEMODE (BIOS or DIRECT)
        in the DOS environment.

Return
        SB_OK

───────────────────────────────────────────────────────────────────────────
Function
        sb_wa--write an attribute
        sb_wc--write a character
        sb_wca--write a character and attribute

Synopsis

            #include <local\sbuf.h>
            extern struct BUFFER Sbuf;
            extern union CELL Scrnbuf[SB_ROWS][SB_COLS];

            int sb_wa(win, attr, n)
            int sb_wc(win, ch, n)
            int sb_wca(win, ch, attr, n)
            union REGION *win;              /* target window */
            unsigned char ch;               /* character */
            unsigned char attr;             /* video attribute */
            short n;                        /* number of repetitions */

Description
        These functions place characters and attributes in the screen-buffer
        array. The window cursor position is not changed. The repetition
        number specifies the number of buffer cells that are affected.

Return
        SB_OK if the write is successful or SB_ERR if an error occurs.

Notes
        These functions are valid only within the current row. Behavior is
        undefined if writes are attempted outside the boundaries of the
        specified window.

═══════════════════════════════════════════════════════════════════════════
UTILITY LIBRARY
───────────────────────────────────────────────────────────────────────────
The development of the utility library starts in Chapter 3 and continues
throughout the book. These routines are usable in a variety of programming
situations and are not easily classified in any of the foregoing
categories.

───────────────────────────────────────────────────────────────────────────
Function
        beep--issue a standard terminal beep (Ctrl-G)

Synopsis

            void beep()

Description
        Call beep() to sound the PC's internal speaker.

───────────────────────────────────────────────────────────────────────────
Function
        byte2hex--convert a byte value to hexadecimal

Synopsis

            char *byte2hex(data, buf)
            unsigned char data;             /* 1-byte data item */
            char *buf;                      /* hex string buffer */

Description
        The byte2hex() function converts a 1-byte numeric value to a 2-byte
        hexadecimal representation.

Return
        A pointer to the resulting hexadecimal string.

Notes
        The calling function must provide a buffer that is large enough to
        receive the 2-byte string and the terminating null byte.

───────────────────────────────────────────────────────────────────────────
Function
        clrprnt--clear printer to default settings

Synopsis

            #include <local\printer.h>

            int clrprnt(fout)
            FILE *fout;

Description
        The clrprnt() function sends codes needed to set an attached printer
        back to its power-on default values for the major print modes. It does
        so by individually clearing each mode rather than sending a printer
        reset command to avoid "paper creep" and alteration of the top-of-form
        setting.

Return
        SUCCESS if all goes well or FAILURE if any attempt at writing to the
        output stream fails.

───────────────────────────────────────────────────────────────────────────
Function
        fatal--print an error message and exit

Synopsis

            void fatal(pname, str, errlvl)
            char *pname;
            char *str;
            int errlvl;

Description
        If an error occurs in a program, fatal() can be called to display an
        error message and then exit with a status (ERRORLEVEL in DOS terms)
        other than 0 to indicate an error. The usual error code is 1, but
        other values can be used if a program can fail in more ways than one.

───────────────────────────────────────────────────────────────────────────
Function
        fcopy--copy input file to output file

Synopsis

            int fcopy(fin, fout)
            FILE *fin, *fout;

Description
        This function copies an input stream (FILE *) to an output stream
        (FILE *) using the standard library functions fgets() and fputs()
        and a private buffer of BUFSIZ bytes. The ferror() macro is used to
        check for input errors and testing is done for the unambiguous EOF
        on the output stream.

Return
        A successful file copy operation produces a 0 return value. Any input
        or output error that is detected causes a nonzero return.

───────────────────────────────────────────────────────────────────────────
Function
        fixtabs--set fixed-interval tab stops

Synopsis

            void fixtabs(interval)
            int interval;           /* tabbing interval */

Description
        A set of fixed tab stops can be set using fixtabs() to initialize the
        private Tabstops array and then set tab stops at the specified
        intervals. Internally, the Tabstops array has tab stops at column 0
        and repeating at the specified interval. Standard terminal usage
        has an interval of 8 columns.

───────────────────────────────────────────────────────────────────────────
Function
        getname--extract filename[.ext] from a pathname

Synopsis

            char *getname(path)
            char *path;

Description
        This function extracts a full filespec (filename[.ext]) from a
        pathname. It checks to see that the pathname given as a parameter is a
        valid pathname. Path separators of \ and / are accepted and may be
        intermixed.

Return
        A pointer to a valid filespec or NULL if the path parameter is not a
        valid pathname.

───────────────────────────────────────────────────────────────────────────
Function
        getopt--get option letters and option arguments

Synopsis

            extern int optind;
            extern int opterr;
            extern char *optarg;

            int getopt(argc, argv, opts)
            int argc;               /* argument count */
            char *argv[];           /* argument vector array */
            char *opts;             /* option string */

Description
        The string variable opts is a list of recognized option letters.
        Option letters that are followed by a colon are expected to take an
        argument. On each successive call to getopt(), the function returns
        the next option letter in argv that matches a letter in opts. The
        optarg variable points to an option argument, if any is expected and
        found or NULL otherwise.

        When all options have been processed, getopt() returns EOF. The
        special option "--" (double dash) may be used to mark the end of
        options. When found, this causes getopt() to skip the special option
        and return EOF.

Return
        If an option letter in argv is not found in the opts string, getopt()
        returns a question mark and prints an error message. Setting opterr to
        0 silences getopt().

Notes
        This getopt() is the public domain version that is provided by the
        AT&T UNIX System Toolchest. It is presented in this book with the
        permission of AT&T in the interest of promoting a common command-line
        interface for programs.

───────────────────────────────────────────────────────────────────────────
Function
        getpname--extract a program name from a pathname

Synopsis

            void getpname(path, pname)
            char *path;             /* full or relative pathname */
            char *pname;            /* filename part of program name */

Description
        The getpname() function receives a full or relative pathname. It
        strips off any leading path information and any trailing extension.
        This function accepts both \ and / as path separators. It does not
        check to see that the path parameter is a valid path.

───────────────────────────────────────────────────────────────────────────
Function
        getxline--get an expanded line of text

Synopsis

            char *getxline(buf, size, fin)
            char *buf;              /* output buffer */
            int size;               /* maximum number of bytes */
            FILE *fin;              /* input stream */

Description
        The getxline() function receives a line of text as input and "expands"
        the line by converting each tab to the correct number of spaces. The
        total character count may thus be altered, but the expanded line
        appears to be visually identical to the input line when displayed.

Return
        A pointer to the buffer that contains the expanded line or NULL if an
        error occurs.

Notes
        This function queries the tabstop array, so you must first call either
        fixtabs() or vartabs() to set tabs.

───────────────────────────────────────────────────────────────────────────
Function
        fconfig--get pointer to a configuration file

Synopsis

            FILE *fconfig(varname, fname)
            char *varname;          /* DOS environment variable name */
            char *fname;            /* file name */

Description
        This function looks for a configuration file by the name fname. It
        looks first in the current directory and then in a directory pointed
        to by the varname DOS variable.

Return
        A pointer to the configuration file if one is found or a NULL pointer
        if not.

───────────────────────────────────────────────────────────────────────────
Function
        last_ch--get the last character of a string

Synopsis

            char last_ch(s)
            char *s;                /* source string */

Description
        The last_ch() function finds the last character of the source string,
        s (just before the NUL termination).

Return
        The character value or -1 if the string is empty.

───────────────────────────────────────────────────────────────────────────
Function
        lines--send blank lines to an output stream

Synopsis

            int lines(n, fp)
            int n;                  /* number of repetitions */
            FILE *fp;               /* output stream */

Description
        The lines() function places n newline characters on the output stream.

Return
        The number of newlines actually emitted, which may be less than the
        number requested if an error occurs on the output stream.

───────────────────────────────────────────────────────────────────────────
Function
        memchk--check for physical memory at an address

Synopsis

            int memchk(seg, os)
            unsigned int seg;       /* segment */
            unsigned int os;        /* offset */

Description
        By sequentially writing and reading the memory location specified by
        the segmented address, memchk() attempts to determine whether any
        physical memory is installed. To prevent clobbering needed memory
        values, the original value is preserved and restored upon completion
        of the test.

Return
        If active memory is found at the specified address, memchk() returns a
        1. If no active memory is found, 0 is returned.

───────────────────────────────────────────────────────────────────────────
Function
        mkslist--create a selection list (lookup table)

Synopsis

            extern long Highest;

            int mkslist(list)
            char *list;             /* pointer to a list in string form */

Description
        The mkslist() function extracts tokens from a string representation of
        a list of numbers, converts the tokens to numeric values, and stores
        the values as a set of numeric ranges in an array (up to 10 ranges
        permitted by the current design).

        Single numbers are represented as ranges having the same starting and
        ending number. Open-ended ranges (for example, 3-) are represented as
        the starting number to some unreasonably high ending value (BIGGEST).
        The list of ranges is terminated by a -1 starting value in the last
        array element following the last valid range.

Return
        Always zero.
───────────────────────────────────────────────────────────────────────────
Function
        nlerase--erase a newline in a string

Synopsis

            char *nlerase(s)
            char *s;

Description
        The nlerase() function scans a string and replaces the first newline
        it finds with a NUL (\0) character, effectively terminating the
        string and erasing the newline.

Return
        A pointer to the modified string.

───────────────────────────────────────────────────────────────────────────
Function
        selected--determine whether a number is in a range

Synopsis

            int selected(n)
            long n;                 /* number to test */

Description
        The selected() function queries the select-list array, Slist, which is
        established by mkslist(), to determine whether the number parameter is
        a member of one of the ranges in the list.

Return
        Returns 1 if the number is in one of the ranges and 0 otherwise.

───────────────────────────────────────────────────────────────────────────
Function
        setfont--select a printer font

Synopsis

            #include <local\printer.h>

            int setfont(ftype, fout)
            int ftype;              /* font specifier */
            FILE *fout;             /* output stream */

Description
        This function is used to select a printer font type. The standard font
        is a 10 cpi (characters per inch) type and it can be obtained by
        list calling clrprnt(). The special types are chosen from the
        following list (based on Epson MX/FX-series printer codes):

───────────────────────────────────────────────────────────────────────────
Symbolic Name       Description
───────────────────────────────────────────────────────────────────────────
CONDENSED           A small but very readable type at about 14 cpi (136
                    characters per line)
DOUBLE              Double strike
EMPHASIZED          Emboldened by microspacing and restriking
EXPANDED            Wider than normal characters (6 cpi)
ITALICS             True italic characters (not supported by all Epson-
                    compatible printers)
UNDERLINE           Continuous underline (without the need to backspace and
                    restrike the character)
───────────────────────────────────────────────────────────────────────────

Return
        The setfont() function returns SUCCESS to indicate apparent success,
        or FAILURE if an error occurs or an illegal font combination is
        requested.

Notes
        The CONDENSED font cannot be used with either DOUBLE or EMPHASIZED on
        most Epson-compatible printers, so such combinations are disallowed.

───────────────────────────────────────────────────────────────────────────
Function
        setprnt--install printer codes

Synopsis

            #include <local\printer.h>

            int setprnt()

Description
        This function installs printer codes from printer configuration files
        or from internal defaults (for Epson MX/FX series and compatible
        printers).

Return
        The setprnt() function returns SUCCESS to indicate apparent success,
        or FAILURE if an error occurs. The only detectable failures are too
        many or too few lines in a configuration file.

───────────────────────────────────────────────────────────────────────────
Function
        spaces--send spaces to an output stream

Synopsis

            int spaces(n, fp)
            int n;                  /* number of repetitions */
            FILE *fp;               /* output stream */

Description
        The spaces() function places n space (blank) characters on the output
        stream.

Return
        The number of spaces actually emitted, which may be less than the
        number requested if an error occurs on the output stream.

───────────────────────────────────────────────────────────────────────────
Function
        tabstop--determine whether col is a tab stop

Synopsis

            int tabstop(col)
            register int col;

Description
        The tabstop() function queries the private Tabstops array to determine
        whether the specified column, col, is a tab stop.

Return
        A nonzero value if col is a tab stop or 0 if it is not.

───────────────────────────────────────────────────────────────────────────
Function
        vartabs--set variable tabs from a list

Synopsis

            void vartabs(list)
            int *list;              /* pointer to an array of tab stops */

Description
        A set of tab stops can be set from a list using vartabs() to
        initialize the private Tabstops array and then set the specified tabs.

───────────────────────────────────────────────────────────────────────────
Function
        word2hex--convert a word value to hexadecimal

Synopsis

            char *word2hex(data, buf)
            unsigned short data;            /* 2-byte data item */
            char *buf;                      /* hex string buffer */

Description
        The word2hex() function converts a 2-byte numeric value to a 4-byte
        hexadecimal representation.

Return
        A pointer to the resulting hexadecimal string.

Notes
        The calling function must provide a buffer that is large enough to
        receive the 4-byte string and the terminating null byte.
───────────────────────────────────────────────────────────────────────────



Augie Hansen
        Augie Hansen started programming on IBM mainframes more
than 20 years ago and since then has been involved with
computers and programming at various companies, including
General Dynamics, Raytheon Company, and E.G. & G., Inc. In
addition, Augie spent 7 years with AT&T Bell Laboratories,
specializing in UNIX and C programming. He founded and is
the president of Omniware, a company that provides academic
and commercial training courses on UNIX, C, and MS/PC-DOS
and custom programming consulting. Augie is a contributing
editor of PC Tech Journal and has written one other book,
vi--The UNIX Screen Editor, published in 1986 by Brady
Books/Prentice-Hall Press.



FIGURE 6-2 ▐ Manual page for CAT
───────────────────────────────────────────────────────────────────────────

NAME

        CAT--concatenate file

FORMAT

        cat [ -s ] [ d: [ path [ filename [ .ext] ] ] ... ]

DESCRIPTION

        The CAT command accepts the -s option (silent), which tells the
        program not to complain about missing files (this may be useful in
        batch files). If no filenames are specified, CAT reads from the
        standard input. DOS wildcard characters (? and *) may be used to
        specify ambiguous filenames.


EXAMPLES

        Display the contents of a single file.

        cat hello.c

        Concatenate all C source files in the current directory into a single
        source file.

        cat *.c >program.c

        Create a new file with text typed by the user.

        cat >myfile.txt
        This is a test.
        ^Z



FIGURE 6-3 ▐ Manual page for TIMER
───────────────────────────────────────────────────────────────────────────

NAME

        TIMER--provide timing data for up to four events

SYNOPSIS

        timer [-efs#]

DESCRIPTION

        TIMER uses command-line options to select program actions. The option
        letters may be combined following a single option flag (-) or invoked
        separately. If the f option is combined with other options, it must be
        last. These options are honored by TIMER:

        ══════════════════════════════════════════════════════════════════════
        Option     Meaning
        ──────────────────────────────────────────────────────────────────────

        -s         Start (or restart) a timer
        -e         Display or record elapsed time
        -f file    Record the output of the TIMER command in file
        -#         The # symbol is a numeric parameter in the range of 0 to 3
                that selects one of the four timers. If no number is
                specified, TIMER uses 0 by default.
        ──────────────────────────────────────────────────────────────────────

EXAMPLES

        Start the default timer:

        timer -s

        Restart timer 0 and record the output in the file project.dat:

        timer -s -f project.dat

NOTES

        TIMER uses the intra-application communication area to hold starting
        times for each of the timers. If another program uses the same memory
        locations, timer data could be corrupted. When asked to produce an
        elapsed time, TIMER tries to detect corrupt data and outputs an error
        message rather than a time value.



FIGURE 7-1 ▐ Manual page for MX
───────────────────────────────────────────────────────────────────────────

NAME

        MX--control printer modes

SYNOPSIS

        mx -option(s)

DESCRIPTION

        The MX program sends mode-setting control strings to a printer under
        control of options. Most of the modes may be set individually or in
        combinations of two or more. It should be obvious that normal (-n)
        should be used by itself. Less obvious is that condensed (-c) and
        either bold (-b) or double-strike (-d) cannot be used together. Within
        the limitations noted above, options listed below may be used singly
        or combined in any order:

═══════════════════════════════════════════════════════════════════════════
        Option     Meaning
        ──────────────────────────────────────────────────────────────────────

        -b         Select bold (emphasized) mode
        -c         Select compressed mode
        -d         Select double-strike mode
        -e         Select expanded mode
        -i         Select italic font
        -n         Select normal font. This command option clears all special
                font and print-mode selections.
        -o file    Use the specified output file or stream
        -p         Preview output on screen (may be redirected)
        -r         Issue a hardware reset command. This clears all attributes
                and establishes the current position as the new top-of-form
                reference.
        -t         Eject a page (top-of-form command) by issuing a formfeed
        -u         Select underline mode
        ______________________________________________________________________

        The reset (-r) and top-of-form (-t) options have temporal priority
        over any other options invoked in the same call to MX. Reset is done
        first, then formfeed, then font selection. Printer configuration can
        be controlled via optional local and global configuration files (local
        overrides global).

EXAMPLES

        Request compressed and italicized printing:

        mx -ci

        Eject a page:

        mx -t



FIGURE 7-2 ▐ Manual page for PRTSTR
───────────────────────────────────────────────────────────────────────────

NAME

        PRTSTR--print string(s) on standard printer

SYNOPSIS

        prtstr [options ] [ string ... ]

DESCRIPTION

        Use PRTSTR to send a string or group of strings given as arguments to
        the standard printer device (stdprn). PRTSTR appends one space after
        each argument and issues a newline to terminate the line. Use quotes
        to protect embedded special characters, such as tabs or spaces, in the
        text-argument list. The following options may be used separately or
        together to alter the default behavior:

        ══════════════════════════════════════════════════════════════════════
        Option     Meaning
        ──────────────────────────────────────────────────────────────────────

        -n         Suppress newline and trailing space.
        -p         Preview output on screen (may be redirected).
        ──────────────────────────────────────────────────────────────────────

EXAMPLES

        Send the line C is a fantastic language! to the standard printer
        device (notice that no quotes are needed):

        prtstr C is a fantastic language!

        Send the string BASEBALL to a file:

        prtstr -p BASEBALL > baseball.txt

        Print some text and stop on the same line:

        prtstr -n "This is a test: "



FIGURE 8-1 ▐ Manual page for TOUCH
───────────────────────────────────────────────────────────────────────────

NAME

        TOUCH--update file modification time(s)

SYNOPSIS

        touch [-cv] file ...

DESCRIPTION

        The TOUCH command updates the file modification times associated with
        a file or files. Under DOS, the last modification time (and last
        access time for DOS version 3.00 and later) maintained in the
        directory entry for a file are identical. TOUCH sets the file time to
        the current DOS time. TOUCH accepts the following command-line
        options, singly or in combination:

        ══════════════════════════════════════════════════════════════════════
        Option     Meaning
        ──────────────────────────────────────────────────────────────────────

        -c         Control file creation. TOUCH usually creates a named file
                if it does exist. This option tells TOUCH not to create
                files.
        -v         Verbose mode. Tells TOUCH to send information to the
                standard error stream about file times that have been
                successfully updated, about files that were created, and so
                forth.
        ──────────────────────────────────────────────────────────────────────

EXAMPLES

        Update the modification times of all C source files in the current
        directory and tell the user what's going on:

        touch -v *.c


        Create a set of empty files in the current directory (assumes these
        files do not exist yet):

        touch file1.c file2.c file3.c



FIGURE 8-2 ▐ Manual page for TEE
───────────────────────────────────────────────────────────────────────────

NAME

        TEE--split a stream into multiple streams

SYNOPSIS

        tee [-a] [file ... ]

DESCRIPTION

        TEE copies its standard input onto its standard output. If any files
        are named, they too become destinations for the output of TEE. The
        files are created if necessary. If they already exist, their prior
        contents are lost unless the -a option (append) is given on the
        command line.

EXAMPLES

        Display the contents of a file (FILE1) and simultaneously copy it to
        another file (FILE2):

        type file1 tee file2

        Use CAT to display the file timer.c on the console and append
        a copy of the output to two existing files (SRCFILE1 and
        SRCFILE2):

        cat timer.c tee -a srcfile1 srcfile2

NOTES

        TEE does not work with files that contain binary data. As designed, it
        handles only ASCII text.

        The number of destination files is limited by the operating system and
        by the use of the value _NFILE in the stdio.h header file (_NFILE is
        typically 20). There are five opened files (stdin, stdout, stderr,
        stdaux, and stdprn) when a program starts executing, which reduces the
        number of files you can open.



FIGURE 8-3 ▐ Manual page for PWD
───────────────────────────────────────────────────────────────────────────

NAME

        PWD--print working directory


SYNOPSIS

        pwd

DESCRIPTION

        PWD displays the full pathname of the current ("working") directory.
        It is designed to ease the transition between UNIX/XENIX and DOS. It
        provides the same function as the DOScommand CD if CD is typed without
        a pathname argument.

EXAMPLE

        Display the current directory pathname:

        pwd

NOTES

        Under UNIX and XENIX, the CD command typed without a pathname argument
        moves the user's context to the "home" directory, which might surprise
        an experienced DOS user trying to get the current pathname by the
        standard technique.



FIGURE 8-4 ▐ Manual page for RM
───────────────────────────────────────────────────────────────────────────

NAME

        RM--remove file(s)

SYNOPSIS

        rm [-i] file ...

DESCRIPTION

        RM removes a file or a group of files. Each file specification may be
        a full or relative pathname and may contain wildcards to name files
        ambiguously.
        The -i option causes RM to operate in an interactive mode. The program
        prompts the user to confirm each removal before it is executed. A
        response that starts with y or Y for "yes" confirms the removal. Any
        other initial character is interpreted as a "no" response.

EXAMPLES

        Remove all backup files in the current directory:

        rm *.bak

        Remove object files and "map" files in the current directory
        interactively:

        rm -i *.obj *.map



FIGURE 8-5 ▐ Manual page for LS
───────────────────────────────────────────────────────────────────────────

NAME

        LS--list information about a directory

SYNOPSIS

        ls [-aCFlrt] [filespec]

DESCRIPTION

        The LS command is a general-purpose directory lister. It has enough
        features to be useful without being threateningly complex. The most
        common use of LS is to find out what files are in the current
        directory. This requires no argument on the command line. To get
        greater amounts of detail or to modify the output of LS in some way,
        use one or more of the following options separately or grouped in any
        meaningful combination.

        ══════════════════════════════════════════════════════════════════════
        Option     Meaning
        ──────────────────────────────────────────────────────────────────────

        -a         All files in the named directory are listed, even those
                marked hidden or system. The default is to shield hidden
                and system files from view.
        -C         Produce multi-column output with items sorted down each
                column. Without this option, single-column output is
                employed. This option causes output to be in the largest
                number of columns possible (determined by the length of the
                longest item name).
        -F         Show file type in the output listing. Directories are
                marked by a trailing backslash (mydir\).
        -l         Produce a single-column "long" listing, including a summary
                of mode bits, file date and time stamp, and file size in
                bytes. This option overrides the -C and -F options.
        -r         Reverse the sorting order. Works for the default sorting of
                file and directory names and for the sort on file
                modification time data (see -t option).
        -t         Sort by file time rather than name. The default is oldest
                first, but most-recent-first may be obtained by combining
                this option with -r.
        ──────────────────────────────────────────────────────────────────────

EXAMPLES

        Display a long list of the current directory:

        ls -l

        Display a multi-column listing of the root directory on the current
        disk from anywhere in the directory hierarchy:

        ls -C \



FIGURE 9-1 ▐ Manual page for PR
───────────────────────────────────────────────────────────────────────────

NAME

        PR--print file(s)

SYNOPSIS

        pr [ options ] [ file ... ]

DESCRIPTION

        The PR program takes a list of filenames and sends the contents of
        each in turn to the standard output. Formatting of the output "pages"
        is controlled via built-in defaults and may be modified by external
        configuration files and command-line options. If no filenames are
        specified, PR reads from its standard input (usually the keyboard,
        unless input is redirected).

        Each page starts with a header composed of the filename, the file-
        relative page number, and the file's modification date and time. When
        reading from the standard input, the filename is omitted and the
        current date and time are used.

        Long lines are folded onto the next line rather than being truncated.
        The line count is not affected by this. PR continues to track the
        number of logical lines in the source file, not the number of physical
        lines printed.

        The options listed below may be used singly or combined in any
        order.

        ══════════════════════════════════════════════════════════════════════
        Option     Action
        ──────────────────────────────────────────────────────────────────────
        -e          Toggle the Epson-compatible printer mode
        -f          Use a formfeed to eject a page (the default is spaces)
        -h hdr      Replace the filename part of the header with the text
                    given in hdr (must be quoted if it contains white space)
        -l len      Set the page length to len lines (the default is 66)
        -n          Enable the line-numbering feature (the initial setting
                    is off)
        -o cols     Set offset to cols columns from left edge of the paper
        -p          Preview output on screen (may be redirected)
        -s list     Output only those lines selected by the comma-separated
                    list (must be quoted if it contains any white space)
        -w cols     Set the output line width to cols columns
        ──────────────────────────────────────────────────────────────────────

        Program configuration may be controlled via optional local and global
        configuration files (local overrides global). Most settings
        established by configuration files may be altered by the user via the
        command-line arguments listed above. The following listing (comments
        are optional--only the numbers and strings at the beginning of each
        line are required) is the default global configuration file, which, if
        it exists, must reside in a subdirectory pointed to by the CONFIG
        environment variable:


        2    top1
        2    top2
        3    bottom
        132  width
        5    left margin
        3    right margin
        66   lines per page
        6    lines per inch
        1    printer mode (Epson)
        1    line number enabled
        1    use formfeeds
        8    standard tabs
        PRN  output destination

EXAMPLES

        Print all of the C source files in the current directory, with
        linenumbering turned on, to an Epson printer attached to the standard
        printer port:

        pr -en *.c

        Display the contents of the \include\std.h header file
        with an offset of 10 columns and a text file:

        pr -o10 \include\std.h

        Print the file myprog.pas on the default printer starting
        at page 4 and continuing up to and including page 10:

        pr -s4,10 myprog.pas



FIGURE 10-1 ▐ Manual page for DUMP
───────────────────────────────────────────────────────────────────────────

NAME

        DUMP--dump file in hexadecimal and text formats

SYNOPSIS

        dump [ -sv] [file ... ]

DESCRIPTION

        The DUMP program is a stand-alone program that can be used as a
        filter. It receives a stream of data and transforms it into a combined
        hexadecimal and text output stream. Each line of output contains three
        fields: an offset from the start of the incoming stream; a hexadecimal
        display of a 16-byte block of data; and a text representation of the
        same block of bytes.

        The output of DUMP contains non-ASCII characters that may confuse
        printers and other output devices. The -s option causes these to be
        stripped out of the output stream and replaced by dots (.). When this
        option is used, the output of DUMP may be safely sent to any other
        program that can read from its standard input, and to any device that
        can accept an ASCII text stream.

        The -v option tells DUMP to be verbose. For each file given as a
        command-line argument, DUMP outputs a blank line, then the name of the
        file being dumped, and then moves to a new line before outputting the
        formatted file dump. When the standard input is the data source, the
        verbose option is ignored.

EXAMPLES

        Display the converted contents of hex.obj on the
        screen:

        dump hex.obj

        Print the converted contents of PROG.COM:

        dump prog.com > prn



FIGURE 10-3 ▐ Manual page for SHOW
───────────────────────────────────────────────────────────────────────────

NAME

        SHOW--make all characters in a stream visible

SYNOPSIS

        show [-svw] [file ... ]

DESCRIPTION

        The SHOW program is a stand-alone program that can be used as a
        filter. It receives a stream of data and transforms it into a straight
        ASCII output stream. Normal displayable characters pass through
        unchanged, but most nonprintable characters are converted to a two-
        digit hexadecimal form.

        The -s option strips all control codes and IBM extended ASCII
        characters from the text stream, except for the primary format
        effectors (tab and newline). The -v option tells SHOW to be verbose.
        When the -w (word-processing) option is used, a carriage return that
        is not paired with a newline is assumed to be a "soft" return and is
        replaced by a newline. The -w option also causes SHOW to convert all
        extended codes (eighth bit on) to 7-bit ASCII and outputs those whose
        converted values fall into the displayable ASCII range.

EXAMPLES

        Display the contents of a spreadsheet data file:

        show data.wks

        Convert a word processor's formatted document file to straight ASCII
        text and save output in a file:

        show -w letter.doc >letter.txt



FIGURE 13-4 ▐ Manual page for SetColor
───────────────────────────────────────────────────────────────────────────

NAME

        SC--control video attributes

SYNOPSIS

        sc [ attribute ]

        sc [ foreground [ background [ border ] ] ]

DESCRIPTION

        The SetColor program may be used in either batch mode or interactive
        mode to set the foreground, background, and border attributes of a
        color display system. The interactive mode, invoked by typing SC with
        no arguments, uses on-screen instructions to guide the user in
        selecting attributes via function keys. The display is updated with
        each keypress to reflect the current selections.

        The attribute selections in batch mode can be formed from the
        following list of argument values. Only the first three letters are
        significant and case is not relevant.

        ══════════════════════════════════════════════════════════════════════
        Values For     Values For Foreground,
        Attribute      Background and Border
        ──────────────────────────────────────────────────────────────────────

        Normal         Black       Red
        Reverse        Blue        Magenta
                    Green       Brown
                    Cyan        White
        ──────────────────────────────────────────────────────────────────────

        All color specifications can be modified by prefixing bold, bright, or
        light to obtain high-intensity values of the base color. Yellow may be
        used as a synonym for bold brown. If "blink" is enabled (the default),
        selecting a high-intensity background will cause the background to be
        a low-intensity color and the foreground will blink.

EXAMPLES

        Run SetColor in the interactive mode:

        sc

        Request bright white on a blue background with a cyan border:

        sc bold white blue cyan

        Set up a reverse video screen:

        sc reverse

NOTES

        The ANSI.SYS device driver must be loaded into memory. This requires
        DOS version 2.00 or later and the line

        device=ansi.sys

        in the CONFIG.SYS file.

        The border cannot be set on some machines, notably the AT&T PC6300.
        Also, the ANSI.SYS device driver supplied with MS-DOS version 2.11
        erases to Normal attribute (white on black) regardless of the
        specified attribute. However, displayed characters written after the
        erasure will have the specified attributes.



FIGURE 14-1 ▐ Manual page for ViewFile
───────────────────────────────────────────────────────────────────────────

NAME

        VF_a full-screen file-viewing program

SYNOPSIS

        vf [-n] file ...

DESCRIPTION

        ViewFile is a file-viewing program that features fast vertical and
        horizontal scrolling through any text file. VF uses the PC arrow and
        special-purpose keys to move around quickly and easily in a file. Most
        commands have single-letter synonyms.

        Absolute go-to-line and search-for-string commands may also be used to
        seek a position in the file being viewed. Line-numbering is optional;
        it can be turned on from the command line by using the -n option.
        Numbering can also be toggled on and off while VF is running by using
        the N key.

        The following commands (and synonyms) are understood by VF:

        ══════════════════════════════════════════════════════════════════════
        COMMAND          MEANING
        ──────────────────────────────────────────────────────────────────────
        PgUp (U)         Scroll up the file one screen page
        PgDn (D)         Scroll down the file one screen page
        Up arrow (-)     Scroll up in the file one line
        Down arrow (+)   Scroll down in the file one line
        Right arrow (>)  Scroll right by 20 columns
        Left arrow (<)   Scroll left by 20 columns
        Home (B)         Go to beginning of file buffer
        End (E)          Go to end of file buffer
        Alt-g (G)        Go to a specified line in the buffer
        Alt-h (H or ?)   Display the help frame
        Alt-n (N)        Toggle line-numbering feature
        \ (R)            Reverse search for a literal string
        / (S)            Search forward for a literal string
        Esc              Next file from list (quits if none)
        Alt-q (Q)        Quit
        ──────────────────────────────────────────────────────────────────────

        This information is also available on-line in the built-in ViewFile
        help frame.

EXAMPLES

        "View" sequentially all of the C source files in the current
        directory:

        vf -n *.c



std.h
───────────────────────────────────────────────────────────────────────────

/*
    *      std.h
    */

/* data type aliases */
#define META    short
#define UCHAR   unsigned char
#define UINT    unsigned int
#define ULONG   unsigned long
#define USHORT  unsigned short

/* Boolean data type */
typedef enum {
        FALSE, TRUE
} BOOLEAN;

/* function return values and program exit codes */
#define OK      0
#define BAD     1
#define SUCCESS 0
#define FAILURE 1

/* infinite loop */
#define FOREVER while (1)

/* masks */
#define HIBYTE  0xFF00
#define LOBYTE  0x00FF
#define ASCII   0x7F
#define HIBIT   0x80

/* lengths */
#define MAXNAME 8
#define MAXEXT  3
#define MAXLINE 256
#define MAXPATH 64

/* special number */
#define BIGGEST 65535



replay
───────────────────────────────────────────────────────────────────────────

/*
    *      replay -- echo the command-line arguments
    *      to standard output
    */

#include <stdio.h>
#include <stdlib.h>
#include <local\std.h>

main(argc, argv)
int argc;
char **argv;
{
        int i;
        char **p;
        static char pgm[MAXNAME + 1] = { "replay" };

        void getpname(char *, char *);

        /* get program name from DOS (version 3.00 and later) */
        if (_osmajor >= 3)
                getpname(*argv, pgm);

        /* check for arguments */
        if (argc == 1)
                exit(1);        /* none given */

        /* echo the argument list, one per line */
        p = argv;
        printf("%s arguments:\n\n", pgm);
        for (--argc, ++argv; argc > 0; --argc, ++argv)
                printf("argv[%d] -> %s\n", argv - p, *argv);
        exit(0);
} /* end main() */



getpname
───────────────────────────────────────────────────────────────────────────

/*
    *      getpname -- extract the base name of a program from the
    *      pathname string (deletes a drive specifier, any leading
    *      path node information, and the extension)
    */

#include <stdio.h>
#include <ctype.h>

void getpname(path, pname)
char *path;     /* full or relative pathname */
char *pname;    /* program name pointer */
{
        char *cp;

        /* find the end of the pathname string */
        cp = path;      /* start of pathname */
        while (*cp != '\0')
                ++cp;
        --cp;           /* went one too far */

        /* find the start of the filename part */
        while (cp > path && *cp != '\\' && *cp != ':' && *cp != '/')
                --cp;
        if (cp > path)
                ++cp;   /* move to right of pathname separator */

        /* copy the filename part only */
        while ((*pname = tolower(*cp)) != '.' && *pname != '\0') {
                ++cp;
                ++pname;
        }
        *pname = '\0';
}



showenv
───────────────────────────────────────────────────────────────────────────

/*
    *      showenv -- display the values of any DOS variables
    *      named on the invocation command line
    */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <local\std.h>

main(argc, argv, envp)
int argc;
char **argv;
char **envp;
{
        register char *ep;
        static char pgm[MAXNAME + 1] = { "showenv" };
        extern void getpname(char *, char *);

        /* use an alias if one is given to this program */
        if (_osmajor >= 3)
                getpname(*argv, pgm);

        /* if no arguments, show full environment list */
        if (argc == 1)
                for (; *envp; ++envp)
                        printf("%s\n", *envp);
        else {
                /*
                    *  treat all args as DOS variable names and
                    *  display values of only specified variables
                    */
                ++argv; /* skip past command name */
                --argc;
                while (argc > 0) {
                        if ((ep = getenv(strupr(*argv))) == NULL)
                                fprintf(stderr, "%s not defined\n", *argv);
                        else
                                printf("%s=%s\n", *argv, ep);
                        ++argv;
                        --argc;
                }
        }
        exit(0);
}



setmydir
───────────────────────────────────────────────────────────────────────────

/*
    *      setmydir -- try to change the DOS environment
    */

#include <stdio.h>
#include <stdlib.h>
#include <local\std.h>

main(argc, argv)
int argc;
char **argv;
{
        register char **p;
        static char var[] = { "MYDIR" };
        static char pgm[MAXNAME + 1] = { "setmydir" };
        extern void fatal(char *, int);
        extern void getpname(char *, char *);

        /* use an alias if one is given to this program */
        if (_osmajor >= 3)
                getpname(*argv, pgm);

        /* try to add the MYDIR variable to the environment */
        if (putenv("MYDIR=c:\\mydir") == -1)
                fatal(pgm, "Error changing environment", 1);

        /* display the environment for this process */
        for (p = environ; *p; p++) {
                printf("%s\n", *p);
        }
        exit(0);
}



timedata
───────────────────────────────────────────────────────────────────────────

/*
    *      timedata -- time zone and time value tests
    */

#include <stdio.h>
#include <time.h>

main()
{
        long now;
        struct tm *tbuf;

        /* get TZ data into global variables */
        tzset();

        /* display the global time values */
        printf("daylight savings time flag = %d\n", daylight);
        printf("difference (in seconds) from GMT = %ld\n", timezone);
        printf("standard time zone string is %s\n", tzname[0]);
        printf("daylight time zone string is %s\n", tzname[1]);

        /*
            *  display the current date and time values for
            *  local and universal time
            */
        now = time(NULL);
        printf("\nctime():\t%s\n", ctime(&now));
        tbuf = localtime(&now);
        printf("local time:\t%s\n", asctime(tbuf));
        tbuf = gmtime(&now);
        printf("universal time:\t%s\n", asctime(tbuf));

        exit(0);
}



notes1
───────────────────────────────────────────────────────────────────────────

/*
    *      notes1 -- add an entry to a "notes" text file
    *
    *      version 1: appends new data to NOTES.TXT in the
    *      current directory -- uses local date/time stamp
    *      as a header for each new entry
    */

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <local\std.h>

main()
{
        FILE *fp;
        static char notesfile[MAXPATH + 1] = { "notes.txt" };
        char ch;
        long ltime;
        static char pgm[MAXNAME + 1] = { "notes1" };

        extern void fatal(char *, char *, int);

        /* try to open notes file in current directory */
        if ((fp = fopen(notesfile, "a")) == NULL)
                fatal(pgm, notesfile, 1);

        /* append a header and date/time tag */
        ltime = time(NULL);
        fprintf(stderr, "Appending to %s: %s\n",
                    notesfile, ctime(&ltime));
        fprintf(fp, "%s\n", ctime(&ltime));

        /* append new text */
        while ((ch = getchar()) != EOF)
                putc(ch, fp);

        /* clean up */
        if (fclose(fp))
                fatal(pgm, notesfile, 2);
        exit(0);
}



notes2
───────────────────────────────────────────────────────────────────────────

/*
    *      notes2 -- add a date/time stamped entry to a
    *      "notes" data file.  Allow user to optionally
    *      edit the data file upon completion of the entry.
    */

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <signal.h>
#include <process.h>
#include <local\std.h>

/* length of date/time string */
#define DT_STR  26

main(argc, argv)
int argc;
char **argv;
{
        int n;          /* number of lines added */
        int exitcode = 0;
        FILE *fp;
        static char notesfile[MAXPATH + 1] = { "notes.txt" };
        static char editname[MAXPATH + 1] = { "edlin" };
        char ch;
        char dt_stamp[DT_STR];
        char *s;
        long ltime;
        static char pgm[MAXNAME + 1] = { "notes2" };

        extern void fatal(char *, char *, int);
        extern void getpname(char *, char *);
        static int addtxt(FILE *, FILE *);

        if (_osmajor >= 3)
                getpname(*argv, pgm);

        /* locate the "notes" database file and open it */
        if (argc > 1)
                strcpy(notesfile, *++argv);
        else if (s = getenv("NOTESFILE"))
                strcpy(notesfile, s);
        if ((fp = fopen(notesfile, "a")) == NULL)
                fatal(pgm, notesfile, 1);

        /* disable Ctrl-Break interrupt */
        if (signal(SIGINT, SIG_IGN) == (int(*)())-1)
                perror("Cannot set signal");

        /* append a header and date/time tag */
        ltime = time(NULL);
        strcpy(dt_stamp, ctime(&ltime));
        fprintf(stderr, "Appending to %s: %s", notesfile, dt_stamp);
        fprintf(fp, "\n%s", dt_stamp);

        /* add text to notes file */
        if ((n = addtxt(stdin, fp)) == 0) {
                fputs("No new text", stderr);
                if (fclose(fp))
                        fatal(pgm, notesfile, 2);
                exit(0);
        }
        else
                fprintf(stderr, "%d line(s) added to %s\n", n, notesfile);
        if (fclose(fp))
                fatal(pgm, notesfile, 2);

        /* optionally edit text in the notes file */
        fprintf(stderr, "E + ENTER to edit; ENTER alone to quit: ");
        while ((ch = tolower(getchar())) != '\n')
                if (ch == 'e') {
                        if (s = getenv("EDITOR"))
                                strcpy(editname, s);
                        if ((exitcode = spawnlp(P_WAIT, editname, editname,
                                notesfile, NULL)) == -1)
                                fatal(pgm, editname, 3);
                }
        exit(exitcode);
}

/*
    *      addtxt -- append new text to notes file
    */
static int addtxt(fin, fout)
FILE *fin, *fout;
{
        int ch;
        int col = 0;    /* column */
        int n = 0;      /* number of lines added */

        while ((ch = fgetc(fin)) != EOF) {
                if (ch == '.' && col == 0) {
                        ch = fgetc(fin); /* trash the newline */
                        break;
                }
                fputc(ch, fout);
                if (ch == '\n') {
                        col = 0;
                        ++n;
                }
                else
                        ++col;
        }
        return (n);
}



run_once
───────────────────────────────────────────────────────────────────────────

/*
    *      run_once -- run a program one time and then
    *      "hang" the system to prevent unwanted use
    */

#include <stdio.h>
#include <stdlib.h>
#include <process.h>
#include <local\std.h>

main(argc, argv)
int argc;
char *argv[];
{
        extern void fatal(char *, *, int);

        ++argv;         /* skip over the program name */

        /* run the specified command line [pgmname arg(s)] */
        if (spawnvp(P_WAIT, *argv, argv) == -1)
                fatal("run_once" "Error running specified program", 1);
        fprintf(stderr, "Please turn off the power to the computer.\n");

        FOREVER         /* do nothing */
                    ;
}



dos.h
───────────────────────────────────────────────────────────────────────────

/*
    * dos.h
    *
    * Defines the structs and unions used to handle the input and output
    * registers for the DOS interface routines defined in the V2.0 to V3.0
    * compatibility package.  It also includes macros to access the segment
    * and offset values of MS C "far" pointers, so that they may be used by
    * these routines.
    *
    * Copyright (C) Microsoft Corporation, 1984, 1985, 1986
    */

/* word registers */

struct WORDREGS {
        unsigned int ax;
        unsigned int bx;
        unsigned int cx;
        unsigned int dx;
        unsigned int si;
        unsigned int di;
        unsigned int cflag;
        };

/* byte registers */

struct BYTEREGS {
        unsigned char al, ah;
        unsigned char bl, bh;
        unsigned char cl, ch;
        unsigned char dl, dh;
        };

/* general purpose registers union - overlays the corresponding word and
    * byte registers.
    */

union REGS {
        struct WORDREGS x;
        struct BYTEREGS h;
        };

/* segment registers */

struct SREGS {
        unsigned int es;
        unsigned int cs;
        unsigned int ss;
        unsigned int ds;
        };

/* dosexterror struct */

struct DOSERROR {
        int exterror;
        char class;
        char action;
        char locus;
        };

/* macros to break MS C "far" pointers into their segment and offset
    * components
    */

#define FP_SEG(fp) (*((unsigned *)&(fp) + 1))
#define FP_OFF(fp) (*((unsigned *)&(fp)))

/* function declarations for those who want strong type checking
    * on arguments to library function calls
    */

#ifdef LINT_ARGS        /* argument checking enabled */

#ifndef NO_EXT_KEYS     /* extended keywords are enabled */
int cdecl bdos(int, unsigned int, unsigned int);
int cdecl dosexterr(struct DOSERROR *);
int cdecl intdos(union REGS *, union REGS *);
int cdecl intdosx(union REGS *, union REGS *, struct SREGS *);
int cdecl int86(int, union REGS *, union REGS *);
int cdecl int86x(int, union REGS *, union REGS *, struct SREGS *);
void cdecl segread(struct SREGS *);
#else                   /* extended keywords not enabled */
int bdos(int, unsigned int, unsigned int);
int dosexterr(struct DOSERROR *);
int intdos(union REGS *, union REGS *);
int intdosx(union REGS *, union REGS *, struct SREGS *);
int int86(int, union REGS *, union REGS *);
int int86x(int, union REGS *, union REGS *, struct SREGS *);
void segread(struct SREGS *);
#endif  /* NO_EXT_KEYS */

#else

#ifndef NO_EXT_KEYS     /* extended keywords are enabled */
int cdecl bdos();
int cdecl dosexterr();
int cdecl intdos();
int cdecl intdosx();
int cdecl int86();
int cdecl int86x();
void cdecl segread();
#else                   /* extended keywords not enabled */
void segread();
#endif  /* NO_EXT_KEYS */

#endif  /* LINT_ARGS */



doslib.h
───────────────────────────────────────────────────────────────────────────
/*
    *      doslib.h
    */

/* DOS interrupts */
#define PGM_TERMINATE    0x20
#define BDOS_REQ         0x21
#define TERMINATE_ADDR   0x22
#define CB_EXIT_ADDR     0x23
#define CRITICAL_ERR     0x24
#define ABS_DISK_READ    0x25
#define ABS_DISK_WRITE   0x26
#define PGM_TERM_RES     0x27

/* DOS functions -- usually placed in ah register for C86 calls */
#define PGM              0x0
#define KEYIN_ECHO_CB    0x1
#define DSPY_CHAR        0x2
#define AUX_IN           0x3
#define AUX_OUT          0x4
#define PRNT_OUT         0x5
#define KEYIN_ECHO       0x6
#define KEYIN            0x7
#define KEYIN_CB         0x8
#define DSPY_STR         0x9
#define KEYIN_BUF        0xA
#define CH_READY         0xB
#define GET_CH           0xC
#define DISK_RESET       0xD
#define SELECT_DISK      0xE
#define OPEN_FILE        0xF
#define CLOSE_FILE       0x10
#define FIRST_FM         0x11
#define NEXT_FM          0x12
#define DELETE_FILE      0x13
#define READ_SEQ         0x14
#define WRITE_SEQ        0x15
#define CREATE_FILE      0x16
#define RENAME_FILE      0x17
        /* 0x18 reserved */
#define CURRENT_DISK     0x19
#define SET_DTA          0x1A
#define DFLT_FAT_INFO    0x1B
#define FAT_INFO         0x1C
        /* 0x1D - 0x20 reserved */
#define READ_RANDOM      0x21
#define WRITE_RANDOM     0x22
#define FILE_SIZE        0x23
#define SET_INT_VEC      0x25
#define NEW_PGM_SEG      0x26
#define RAND_BLK_READ    0x27
#define RAND_BLK_WRITE   0x28
#define PARSE_FILENAME   0x29
#define GET_DATE         0x2A
#define SET_DATE         0x2B
#define GET_TIME         0x2C
#define SET_TIME         0x2D
#define TOGGLE_VERIFY    0x2E
#define GET_DTA          0x2F
#define DOS_VERSION      0x30
#define PGM_TERM_KEEP    0x31
        /* 0x32 reserved */
#define CB_CHECK         0x33
        /* 0x34 reserved */
#define GET_INTR         0x35
#define GET_FREE_SPACE   0x36
        /* 0x37 reserved */
#define INTL_INFO        0x38
#define MKDIR            0x39
#define RMDIR            0x3A
#define CHDIR            0x3B
#define CREAT            0x3C
#define OPEN_FD          0x3D
#define CLOSE_FD         0x3E
#define WRITE_FD         0x40
#define UNLINK           0x41
#define LSEEK            0x42
#define CHMOD            0x43
#define IOCTL            0x44
#define DUP              0x45
#define FORCE_DUP        0x46
#define GET_CUR_DIR      0x47
#define ALLOC            0x48
#define FREE             0x49
#define SET_BLOCK        0x4A
#define EXEC             0x4B
#define EXIT             0x4C
#define WAIT             0x4D
#define FIND_FIRST       0x4E
#define FIND_NEXT        0x4F
        /* 0x50 - 53 reserved */
#define VERIFY_STATE     0x54
        /* 0x55 reserved */
#define RENAME           0x56
#define FILE_MTIME       0x57



keydefs
───────────────────────────────────────────────────────────────────────────

/*      keydefs -- values for special keys on IBM PC and clones      */


#define XF      0x100         /* extended key flag */

#define K_F1    59 | XF       /* function keys */
#define K_F2    60 | XF
#define K_F3    61 | XF
#define K_F4    62 | XF
#define K_F5    63 | XF
#define K_F6    64 | XF
#define K_F7    65 | XF
#define K_F8    66 | XF
#define K_F9    67 | XF
#define K_F10   68 | XF

#define K_SF1   84 | XF       /* shifted function keys */
#define K_SF2   85 | XF
#define K_SF3   86 | XF
#define K_SF4   87 | XF
#define K_SF5   88 | XF
#define K_SF6   89 | XF
#define K_SF7   90 | XF
#define K_SF8   91 | XF
#define K_SF9   92 | XF
#define K_SF10  93 | XF

#define K_CF1   94 | XF       /* control function keys */
#define K_CF2   95 | XF
#define K_CF3   96 | XF
#define K_CF4   97 | XF
#define K_CF5   98 | XF
#define K_CF6   99 | XF
#define K_CF7   100 | XF
#define K_CF8   101 | XF
#define K_CF9   102 | XF
#define K_CF10  103 | XF

#define K_AF1   104 | XF      /* alternate function keys */
#define K_AF2   105 | XF
#define K_AF3   106 | XF
#define K_AF4   107 | XF
#define K_AF5   108 | XF
#define K_AF6   109 | XF
#define K_AF7   110 | XF
#define K_AF8   111 | XF
#define K_AF9   112 | XF
#define K_AF10  113 | XF

#define K_HOME  71 | XF       /* cursor keypad (NumLock off; */
#define K_END   79 | XF       /*  not shifted) */
#define K_PGUP  73 | XF
#define K_PGDN  81 | XF
#define K_LEFT  75 | XF
#define K_RIGHT 77 | XF
#define K_UP    72 | XF
#define K_DOWN  80 | XF

#define K_CHOME 119 | XF      /* control cursor keypad */
#define K_CEND  117 | XF
#define K_CPGUP 132 | XF
#define K_CPGDN 118 | XF
#define K_CLEFT 115 | XF
#define K_CRGHT 116 | XF

#define K_CTRLA 1             /* standard control keys */
#define K_CTRLB 2
#define K_CTRLC 3
#define K_CTRLD 4
#define K_CTRLE 5
#define K_CTRLF 6
#define K_CTRLG 7
#define K_CTRLH 8
#define K_CTRLI 9
#define K_CTRLJ 10
#define K_CTRLK 11
#define K_CTRLL 12
#define K_CTRLM 13
#define K_CTRLN 14
#define K_CTRLO 15
#define K_CTRLP 16
#define K_CTRLQ 17
#define K_CTRLR 18
#define K_CTRLS 19
#define K_CTRLT 20
#define K_CTRLU 21
#define K_CTRLV 22
#define K_CTRLW 23
#define K_CTRLX 24
#define K_CTRLY 25
#define K_CTRLZ 26

#define K_ALTA  30 | XF       /* alternate keys */
#define K_ALTB  48 | XF
#define K_ALTC  46 | XF
#define K_ALTD  32 | XF
#define K_ALTE  18 | XF
#define K_ALTF  33 | XF
#define K_ALTG  34 | XF
#define K_ALTH  35 | XF
#define K_ALTI  23 | XF
#define K_ALTJ  36 | XF
#define K_ALTK  37 | XF
#define K_ALTL  38 | XF
#define K_ALTM  50 | XF
#define K_ALTN  49 | XF
#define K_ALTO  24 | XF
#define K_ALTP  25 | XF
#define K_ALTQ  16 | XF
#define K_ALTR  19 | XF
#define K_ALTS  31 | XF
#define K_ALTT  20 | XF
#define K_ALTU  22 | XF
#define K_ALTV  47 | XF
#define K_ALTW  17 | XF
#define K_ALTX  45 | XF
#define K_ALTY  21 | XF
#define K_ALTZ  44 | XF

#define K_ALT1        120 | XF   /* additional alternate key */
#define K_ALT2        121 | XF   /* combinations */
#define K_ALT3        122 | XF
#define K_ALT4        123 | XF
#define K_ALT5        124 | XF
#define K_ALT6        125 | XF
#define K_ALT7        126 | XF
#define K_ALT8        127 | XF
#define K_ALT9        128 | XF
#define K_ALT0        129 | XF
#define K_ALTDASH     130 | XF
#define K_ALTEQU      131 | XF

#define K_ESC         27         /* miscellaneous special keys */
#define K_SPACE       32
#define K_INS         82 | XF
#define K_DEL         83 | XF
#define K_TAB         K_CTRLI
#define K_BACKTAB     K_CTRLO

#define K_CTRL_PRTSC  114 | XF   /* printer echoing toggle */

#define K_RETURN      13         /* return key variations */
#define K_SRETURN     13
#define K_CRETURN     10



bioslib.h
───────────────────────────────────────────────────────────────────────────

/*      bioslib.h      */

#define PRINT_SCRN      0x05    /* BIOS interrupts */
#define TOD_INIT        0x08
#define KEYBD_INIT      0x09
#define DISK_INIT       0x0E
#define VIDEO_IO        0x10
#define EQUIP_CK        0x11
#define MEM_SIZE        0x12
#define DISK_IO         0x13
#define RS232_IO        0x14
#define CASS_IO         0x15
#define KEYBD_IO        0x16
#define PRINT_IO        0x17
#define TOD             0x1A
#define VIDEO_INIT      0x1D
#define GRAPHICS        0x1F

#define SET_MODE        0       /* video routine numbers */
#define CUR_TYPE        1       /* (placed in register AH */
#define CUR_POS         2       /*  before a BIOS interrupt 10H) */
#define GET_CUR         3
#define LPEN_POS        4
#define SET_PAGE        5
#define SCROLL_UP       6
#define SCROLL_DN       7
#define READ_CHAR_ATTR  8
#define WRITE_CHAR_ATTR 9
#define WRITE_CHAR      10
#define PALETTE         11
#define WRITE_DOT       12
#define READ_DOT        13
#define WRITE_TTY       14
#define GET_STATE       15
#define ALT_FUNCTION    18      /* EGA only */
#define WRITE_STR       19      /* AT only */

#define RESET_DISK      0       /* disk routine numbers */
#define DISK_STATUS     1
#define READ_SECTOR     2
#define WRITE_SECTOR    3
#define VERIFY_SECTOR   4
#define FORMAT_TRACK    5

#define KBD_READ        0       /* keyboard routine numbers */
#define KBD_READY       1
#define KBD_STATUS      2



equipchk
───────────────────────────────────────────────────────────────────────────

/*      equipchk -- get equipment list      */

#include <dos.h>
#include <local\bioslib.h>
#include <local\equip.h>

struct EQUIP Eq;

int equipchk()
{
        union REGS inregs, outregs;

        /* call BIOS equipment check routine */
        int86(EQUIP_CK, &inregs, &outregs);
        /* extract data from returned data word */
        Eq.nprint  = (outregs.x.ax & 0xC000) / 0x8000;
        Eq.game_io = ((outregs.x.ax & 0x1000) / 0x1000) ? 1 : 0;
        Eq.nrs232  = (outregs.x.ax & 0x0E00) / 0x0200;
        Eq.ndrive  = ((outregs.x.ax & 0x00C0) / 0x0040) + 1;
        Eq.vmode   = (outregs.x.ax & 0x0030) / 0x0010;
        Eq.basemem = ((outregs.x.ax & 0x000C) / 0x0004) + 1;
        Eq.disksys = outregs.x.ax & 0x0001 == 1;
        return (outregs.x.cflag);
}



video.h
───────────────────────────────────────────────────────────────────────────

/*      video.h      */

/* current video state/mode information */
extern short Vmode;
extern short Vwidth;
extern short Vpage;

#define MAXVMODE  17

/* video limit tables */
extern short Maxrow[MAXVMODE];
extern short Maxcol[MAXVMODE];
extern short Maxpage[MAXVMODE];

/* active display */
#define MONO    1
#define COLOR   2

/* cursor modes */
#define CURSOR_OFF      0
#define CURSOR_ON       1

/* installed display adapters */
#define MDA     1
#define CGA     2
#define EGA     4

/* --- video modes --- */
/* CGA modes */
#define CGA_M40         0
#define CGA_C40         1
#define CGA_M80         2
#define CGA_C80         3
#define CGA_CMRES       4
#define CGA_MMRES       5
#define CGA_MHRES       6
/* MDA mode */
#define MDA_M80         7
/* PCjr modes */
#define PCJR_CLRES      8
#define PCJR_CMRES      9
#define PCJR_CHRES      10
/* modes 11 and 12 are not currently used */
/* EGA modes */
#define EGA_CMRES       13
#define EGA_CHRES       14
#define EGA_MHRES       15
#define EGA_EHRES       16

/* miscellaneous video masks */
#define CMASK   0x00FF    /* character mask */
#define AMASK   0xFF00    /* attribute mask */

/* attribute modifiers */
#define BRIGHT  8
#define BLINK   128

/* primary video attributes */
#define BLU     1
#define GRN     2
#define RED     4

/* composite video attributes */
#define BLK     0
#define CYAN    (BLU | GRN)             /* 3 */
#define MAGENTA (BLU | RED)             /* 5 */
#define BRN     (GRN | RED)             /* 6 */
#define WHT     (BLU | GRN | RED)       /* 7 */
#define GRAY    (BLK | BRIGHT)
#define LBLU    (BLU | BRIGHT)
#define LGRN    (GRN | BRIGHT)
#define LCYAN   (CYAN | BRIGHT)
#define LRED    (RED | BRIGHT)
#define LMAG    (MAG | BRIGHT)
#define YEL     (BRN | BRIGHT)
#define BWHT    (WHT | BRIGHT)
#define NORMAL  WHT
#define REVERSE 112

/*      drawing characters -- items having two numbers use
    *      the first number as the horizontal specifier */

/* single-line boxes */
#define VBAR1   179
#define VLINE   179     /* alias */
#define HBAR1   196
#define HLINE   196     /* alias */
#define ULC11   218
#define URC11   191
#define LLC11   192
#define LRC11   217
#define TL11    195
#define TR11    180
#define TT11    194
#define TB11    193
#define X11     197

/* double-line boxes */
#define VBAR2   186
#define HBAR2   205
#define ULC22   201
#define URC22   187
#define LLC22   200
#define LRC22   188
#define TL22    204
#define TR22    185
#define TT22    203
#define TB22    202
#define X22     206

/* single-line horizontal & double-line vertical boxes */
#define ULC12   214
#define URC12   183
#define LLC12   211
#define LRC12   189
#define TL12    199
#define TR12    182
#define TT12    210
#define TB12    208
#define X12     215

/* double-line horizontal & single-line vertical boxes */
#define ULC21   213
#define URC21   184
#define LLC21   212
#define LRC21   190
#define TL21    198
#define TR21    181
#define TT21    209
#define TB21    207
#define X21     216

/* full and partial blocks */
#define BLOCK   219
#define VBAR    219     /* alias */
#define VBARL   221
#define VBARR   222
#define HBART   223
#define HBARB   220

/* special character-graphic symbols */
#define BLANK           32
#define DIAMOND         4
#define UPARROW         24
#define DOWNARROW       25
#define RIGHTARROW      26
#define LEFTARROW       27
#define SLASH           47



getstate
───────────────────────────────────────────────────────────────────────────
/*      getstate -- update video state structure      */

#include <dos.h>
#include <local\std.h>
#include <local\bioslib.h>

/* current video state/mode information */
short Vmode;
short Vwidth;
short Vpage;

/*      video tables -- these tables of video parameters use
    *      a value of -1 to indicate that an item is not supported
    *      and 0 to indicate that an item has a variable value.      */

/* video limit tables */
short Maxrow[] = {
        25, 25, 25, 25, 25, 25, 25,      /* CGA modes */
        25,                              /* MDA mode */
        25, 25, 25,                      /* PCjr modes */
        -1, -1,                          /* not used */
        25, 25, 25, 43                   /* EGA modes */
};

short Maxcol[] = {
        40, 40, 80, 80, 40, 40, 80,      /* CGA modes */
        80,                              /* MDA mode */
        -1, 40, 80,                      /* PCjr modes */
        -1, -1,                          /* not used */
        80, 80, 80, 80                   /* EGA modes */
};

short Maxpage[] = {
        8, 8, 4, 4, 1, 1, 1,             /* CGA modes */
        1,                               /* MDA mode */
        0, 0, 0,                         /* PCjr modes */
        -1, -1,                          /* not used */
        8, 4, 1, 1                       /* EGA modes */
};

int getstate()
{
        union REGS inregs, outregs;

        inregs.h.ah = GET_STATE;
        int86(VIDEO_IO, &inregs, &outregs);

        Vmode = outregs.h.al;
        Vwidth = outregs.h.ah;
        Vpage = outregs.h.bh;
        return (outregs.x.cflag);
}



setvmode
───────────────────────────────────────────────────────────────────────────

/*     setvmode -- set the video mode (color/graphics systems only)     */

#include <dos.h>
#include <local\std.h>
#include <local\bioslib.h>

/***********************************************************
* mode #       description
* ------------ ----------------------------------
* PC MODES:
*       0       40x25 Mono text (c/g default)
*       1       40x25 Color text
*       2       80x25 Mono text
*       3       80x25 Color text
*       4       320x200 4-color graphics (med res)
*       5       320x200 Mono graphics (med res)
*       6       640x200 2-color graphics (hi res)
*       7       80x25 on monochrome adapter
*
* PCjr MODES:
*       8       160x200 16-color graphics
*       9       320x200 16-color graphics
*       10      640x200 4-color fraphics
*
* EGA MODES:
*       13      320x200 16-color graphics
*       14      620x200 16-color graphics
*       15      640x350 mono graphics
*       16      640x350 color graphics (4- or 16-color)
***********************************************************/

int setvmode(vmode)
unsigned int vmode;     /* user-specified mode number */
{
        union REGS inregs, outregs;

        inregs.h.ah = SET_MODE;
        inregs.h.al = vmode;    /* value not checked */
        int86(VIDEO_IO, &inregs, &outregs);

        getstate();      /* update video structure */
        return (outregs.x.cflag);
}



setpage
───────────────────────────────────────────────────────────────────────────

/*      setpage -- select "visual" screen page for viewing
    *      (the "active" page is the one being written to)      */

#include <dos.h>
#include <local\std.h>
#include <local\bioslib.h>
#include <local\video.h>

int setpage(pg)
int pg; /* visual screen page number */
{
        union REGS inregs, outregs;

        /* check page number against table */
        if (Maxpage[Vmode] > 0 && (pg < 0 || pg >= Maxpage[Vmode]))
                return (-1);

        /* change the visual page */
        inregs.h.ah = SET_PAGE;
        inregs.h.al = pg;
        int86(VIDEO_IO, &inregs, &outregs);
        return (outregs.x.cflag);
}



readcur
───────────────────────────────────────────────────────────────────────────

/*      readcur -- pass back the cursor position (row, col)      */

#include <dos.h>
#include <local\std.h>
#include <local\bioslib.h>

unsigned int readcur(row, col, pg)
unsigned int *row;      /* current row */
unsigned int *col;      /* current column */
unsigned int pg;        /* screen page */
{
        union REGS inregs, outregs;

        inregs.h.ah = GET_CUR;
        inregs.h.bh = pg;

        int86(VIDEO_IO, &inregs, &outregs);

        *col = outregs.h.dl;            /* col */
        *row = outregs.h.dh;            /* row */

        return (outregs.x.cflag);
}



putcur
───────────────────────────────────────────────────────────────────────────

/*      putcur -- put cursor at specified position (row, col)      */

#include <dos.h>
#include <local\std.h>
#include <local\bioslib.h>

unsigned int putcur(r, c, pg)
unsigned int
        r,      /* row */
        c,      /* column */
        pg;     /* screen page for writes */
{
        union REGS inregs, outregs;

        inregs.h.ah = CUR_POS;
        inregs.h.bh = pg & 0x07;
        inregs.h.dh = r & 0xFF;
        inregs.h.dl = c & 0xFF;

        int86(VIDEO_IO, &inregs, &outregs);

        return (outregs.x.cflag);
}



getctype
───────────────────────────────────────────────────────────────────────────

/*      getctype -- pass back cursor type info (scan lines)      */

#include <dos.h>
#include <local\std.h>
#include <local\bioslib.h>

#define LO_NIBBLE       0x0F

int getctype(start_scan, end_scan, pg)
int *start_scan;        /* starting scan line */
int *end_scan;          /* ending scan line */
int pg;                 /* "visual" page */
{
        union REGS inregs, outregs;

        inregs.h.bh = pg;
        inregs.h.ah = GET_CUR;

        int86(VIDEO_IO, &inregs, &outregs);

        /* end_scan = low 4 bits of cl */
        *end_scan = outregs.h.cl & LO_NIBBLE;

        /* starting_scan = low 4 bits of ah */
        *start_scan = outregs.h.ch & LO_NIBBLE;

        return (outregs.x.cflag);
}



setctype
───────────────────────────────────────────────────────────────────────────

/*      setctype -- set the cursor start and end raster scan lines      */

#include <dos.h>
#include <local\bioslib.h>

#define LO_NIBBLE       0x0F
#define CURSOR_OFF      0x2
#define MAXSCANLN       15

int setctype(start, end)
int start;      /* starting raster scan line */
int end;        /* ending raster scan line */
{
        union REGS inregs, outregs;

        inregs.h.ah = CUR_TYPE;
        inregs.h.ch = start & LO_NIBBLE;
        inregs.h.cl = end & LO_NIBBLE;
        if (start >= MAXSCANLN) {
                inregs.h.ah |= CURSOR_OFF;
                inregs.h.al = MAXSCANLN;
        }
        int86(VIDEO_IO, &inregs, &outregs);

        return (outregs.x.cflag);
}



readca
───────────────────────────────────────────────────────────────────────────

/*      readca -- read character and attribute at current position      */

#include <dos.h>
#include <local\std.h>
#include <local\bioslib.h>

int readca(ch, attr, pg)
unsigned char *ch;
unsigned char *attr;
unsigned int pg;        /* screen page for reads */
{
        union REGS inregs, outregs;

        inregs.h.ah = READ_CHAR_ATTR;
        inregs.h.bh = pg;               /* display page */

        int86(VIDEO_IO, &inregs, &outregs);

        *ch = outregs.h.al;             /* character */
        *attr = outregs.h.ah;           /* attribute */

        /* return the value in AX register */
        return (outregs.x.cflag);
}



writeca
───────────────────────────────────────────────────────────────────────────

/*      writeca -- write character and attribute to the screen      */

#include <dos.h>
#include <local\std.h>
#include <local\bioslib.h>

int writeca(ch, attr, count, pg)
unsigned char ch;       /* character */
unsigned char attr;     /* attribute */
int count;              /* number of repetitions */
int pg;                 /* screen page for writes */
{
        union REGS inregs, outregs;

        inregs.h.ah = WRITE_CHAR_ATTR;
        inregs.h.al = ch;
        inregs.h.bh = pg;
        inregs.h.bl = attr;
        inregs.x.cx = count;
        int86(VIDEO_IO, &inregs, &outregs);

        return (outregs.x.cflag);
}



clrscrn
───────────────────────────────────────────────────────────────────────────

/*      clrscrn -- clear the "visual" screen page      */

#include <dos.h>
#include <local\std.h>
#include <local\bioslib.h>
#include <local\video.h>

int clrscrn(a)
unsigned int a;        /* video attribute for new lines */
{
        union REGS inregs, outregs;

        inregs.h.ah = SCROLL_UP;
        inregs.h.al = 0;                 /* blank entire window */
        inregs.h.bh = a;                 /* use specified attribute */
        inregs.h.bl = 0;
        inregs.x.cx = 0;                 /* upper left corner */
        inregs.h.dh = Maxrow[Vmode] - 1; /* bottom screen row */
        inregs.h.dl = Maxcol[Vmode] - 1; /* rightmost column */
        int86(VIDEO_IO, &inregs, &outregs);

        return (outregs.x.cflag);
}



clrw
───────────────────────────────────────────────────────────────────────────

/*      clrw -- clear specified region of "visual" screen page      */

#include <dos.h>
#include <local\std.h>
#include <local\bioslib.h>

int clrw(t, l, b, r, a)
int t;            /* top row of region to clear */
int l;            /* left column */
int b;            /* bottom row */
int r;            /* right column */
unsigned char a;  /* attribute for cleared region */
{
        union REGS inregs, outregs;

        inregs.h.ah = SCROLL_UP; /* scroll visual page up */
        inregs.h.al = 0;         /* blank entire window */
        inregs.h.bh = a;         /* attribute of blank lines */
        inregs.h.bl = 0;
        inregs.h.ch = t;         /* upper left of scroll region */
        inregs.h.cl = l;
        inregs.h.dh = b;         /* lower right of scroll region */
        inregs.h.dl = r;
        int86(VIDEO_IO, &inregs, &outregs);

        return (outregs.x.cflag);
}



scroll
───────────────────────────────────────────────────────────────────────────

/*      scroll -- scroll a region of the "visual" screen
    *      page up or down by n rows (0 = initialize region)      */

#include <dos.h>
#include <local\std.h>
#include <local\bioslib.h>

int scroll(t, l, b, r, n, a)
int t;  /* top row of scroll region */
int l;  /* left column */
int b;  /* bottom row */
int r;  /* right column */
int n;  /* number of lines to scroll */
        /* sign indicates direction to scroll */
        /* 0 means scroll all lines in the region (initialize) */
unsigned char a;/* attribute for new lines */
{
        union REGS inregs, outregs;

        if (n < 0) {
                /* scroll visual page down n lines */
                inregs.h.ah = SCROLL_DN;
                inregs.h.al = -n;
        }
        else {
                /* scroll visual page up n lines */
                inregs.h.ah = SCROLL_UP;
                inregs.h.al = n;
        }
        inregs.h.bh = a;        /* attribute of blank lines */
        inregs.h.bl = 0;
        inregs.h.ch = t;        /* upper-left of scroll region */
        inregs.h.cl = l;
        inregs.h.dh = b;        /* lower-right of scroll region */
        inregs.h.dl = r;
        int86(VIDEO_IO, &inregs, &outregs);

        return (outregs.x.cflag);
}



writea
───────────────────────────────────────────────────────────────────────────

/*      writea -- write attribute only to screen memory (faked by
    *      reading char and attr and writing back the original
    *      character and the new attribute at each position)      */

#include <local\std.h>

int writea(a, n, pg)
unsigned char a;/* video attribute */
int n;          /* number of positions to write */
int pg;         /* screen page */
{
        int i;
        int status;
        unsigned short chx, attrx;
        unsigned short r, c;

        /* get starting (current) position */
        status = 0;
        status = readcur(&r, &c, pg);
        for (i = 0; i < n; ++i) {
                status += putcur(r, c + i, pg);
                status += readca(&chx, &attrx, pg);
                status += writeca(chx, a, 1, pg);
        }

        /* restore cursor position */
        status += putcur(r, c, pg);

        return (status);
}



drawbox
───────────────────────────────────────────────────────────────────────────

/*      drawbox -- create a box with IBM line-drawing characters      */

#include <local\video.h>

int drawbox(top, lft, btm, rgt, pg)
int top, lft, btm, rgt, pg;
{
        int i;
        int x;  /* interior line length for top and bottom segments */

        x = rgt - lft - 1;

        /* draw the top row */
        putcur(top, lft, pg);
        put_ch(ULC11, pg);
        writec(HBAR1, x, pg);
        putcur(top, rgt, pg);
        put_ch(URC11, pg);

        /* draw the sides */
        for (i = 1; i < btm - top; ++i)
        {
                putcur(top + i, lft, pg);
                put_ch(VBAR1, pg);
                putcur(top + i, rgt, pg);
                put_ch(VBAR1, pg);
        }

        /* draw the bottom row */
        putcur(btm, lft, pg);
        put_ch(LLC11, pg);
        writec(HBAR1, x, pg);
        putcur(btm, rgt, pg);
        put_ch(LRC11, pg);
}



cursor
───────────────────────────────────────────────────────────────────────────

/*      cursor -- interactively set cursor shape      */

#include <dos.h>
#include <stdlib.h>
#include <local\video.h>
#include <local\keydefs.h>

/* additional drawing characters (others are defined in video.h) */
#define DOT     254
#define NO_DOT  196
#define D_POINT 31
#define R_POINT 16
#define L_POINT 17

/* dimensions of the help frame */
#define BOX_Y   6
#define BOX_X   30

/* upper-left row and column of big cursor */
int Ulr, Ulc;
int Mid;

/* cursor scan-line-selection modes */
typedef enum { STARTSCAN, ENDSCAN } CMODE;

int main()
{
        int i, j, ch;
        int start, end;
        int height, width;
        static char spoint[] = { "Start\020" }; /* contains right pointer */
        static char epoint[] = { "\021Stop" };  /* contains left pointer */
        static char title[] = { "CURSOR: Control cursor shape (V1.0)" };
        unsigned char
                oldattr,        /* video attribute upon entry */
                headattr,       /* video attribute of header */
                attr,           /* primary video attribute */
                standout;       /* highlighting video attribute */
        CMODE mode;

        static void drawdspy(int, int, int, int, int);
        static void drawstart(int, char *);
        static void drawend(int, int, char *);
        static void drawactive(int, int, CMODE);
        static void showhelp(int, int);
        static void writestr(char *, int);

        /* get video information and initialize */
        getstate();
        Mid = Vwidth / 2;
        readca(&ch, &oldattr, Vpage);  /* preserve user's video attribute */
        getctype(&start, &end, Vpage); /* and cursor shape */
        headattr = (WHT << 4) | BLK;

        /* set parameters based on video mode (default = CGA) */
        height = width = 8;     /* use an 8 by 8 block character cell */
        attr = (BLU << 4) | CYAN | BRIGHT;
        standout = YEL;
        if (Vmode == MDA_M80) {
                /* uses a 14 by 9 dot block character cell */
                height = 14;
                width = 9;
                attr = NORMAL;
                standout = BWHT;
        }
        setctype(height + 1, height + 1);       /* cursor off */

        /* basic text and layout */
        Ulr = 2;
        Ulc = Mid - width / 2;
        clrscrn(attr);
        putcur(0, 0, Vpage);
        writeca(' ', headattr, Vwidth, Vpage);
        putcur(0, Mid - strlen(title) / 2, Vpage);
        writestr(title, Vpage);
        showhelp(Ulr + height + 1, Mid - BOX_X / 2);

        /* interactively select cursor shape */
        mode = STARTSCAN;
        drawdspy(start, end, standout, width, height);
        drawstart(start, spoint);
        drawend(end, width, epoint);
        drawactive(height, width, mode);
        while (1) {
                switch (ch = getkey()) {
                case K_UP:
                        /* move up one scan line */
                        if (mode == STARTSCAN)
                                drawstart(start--, "      ");
                        else
                                drawend(end--, width, "     ");
                        break;
                case K_DOWN:
                        /* move down one scan line */
                        if (mode == STARTSCAN)
                                drawstart(start++, "      ");
                        else
                                drawend(end++, width, "     ");
                        break;
                case K_LEFT:
                        /* starting scan-line-selection mode */
                        mode = STARTSCAN;
                        drawactive(height, width, mode);
                        continue;
                case K_RIGHT:
                        /* ending scan-line-selection mode */
                        mode = ENDSCAN;
                        drawactive(height, width, mode);
                        continue;
                case K_RETURN:
                        /* set the new cursor shape */
                        setctype(start, end);
                        clrscrn(oldattr);
                        putcur(0, 0, Vpage);
                        exit(0);
                }

                /* make corrections at cursor image boundaries */
                if (start < 0)
                        start = 0;
                else if (start > height)
                        start = height;
                if (end < 0)
                        end = 0;
                else if (end >= height)
                        end = height - 1;

                /* show updated cursor shape and pointers */
                drawdspy(start, end, standout, width, height);
                drawstart(start, spoint);
                drawend(end, width, epoint);
        }
        exit(0);
} /* end main() */


/*      drawdspy -- draw a magnified image of a cursor with the
    *      currently active scan lines depicted as a sequence of dots
    *      and inactive lines depicted as straight lines      */

static void drawdspy(s, e, a, w, h)
int s;  /* starting scan line */
int e;  /* ending scan line */
int a;  /* video attribute */
int w;  /* width */
int h;  /* height */
{
        int i;

        /* display an exploded image of each scan line */
        for (i = 0; i < h; ++i) {
                putcur(Ulr + i, Ulc, Vpage);
                if (s >= h)
                        /* cursor is effectively off */
                        writeca(NO_DOT, a, w, Vpage);
                else if ((s <= e && i >= s && i <= e) || /* a full block */
                        (s > e && (i <= e || i >= s)))  /* a split block */
                        writeca(DOT, a, w, Vpage);
                else
                        /* outside start/end range */
                        writeca(NO_DOT, a, w, Vpage);
        }
} /* end drawdspy() */


/*      drawstart -- display a pointer to the displayed starting
    *      scan line in the magnified cursor image      */

static void drawstart(s, sp)
int s;          /* starting scan line number */
char *sp;       /* visual pointer to the displayed starting scan line */
{
        putcur(Ulr + s, Ulc - strlen(sp), Vpage);
        putstr(sp, Vpage);
} /* end drawstart() */


/*      drawend -- display a pointer to the displayed ending
    *      scan line in the magnified cursor image      */

static void drawend(e, w, ep)
int e;          /* ending scan line number */
int w;          /* width of the cursor image */
char *ep;       /* visual pointer to the displayed ending scan line */
{
        putcur(Ulr + e, Ulc + w, Vpage);
        putstr(ep, Vpage);
} /* end drawend() */

static void drawactive(h, w, m)
int h, w;
CMODE m;
{
        int col;

        /* clear active selector row */
        putcur(Ulr - 1, Ulc, Vpage);
        writec(' ', w, Vpage);

        /* point to active selector */
        col = (m == STARTSCAN) ? 0 : w - 1;
        putcur(Ulr - 1, Ulc + col, Vpage);
        writec(D_POINT, 1, Vpage);
} /* end drawactive() */

/      showhelp -- display a set of instructions about the
    *      use of the cursor program in a fine-ruled box      */

static void showhelp(r, c)
int r, c;       /* upper-left corner of help frame */
{
        static char title[] = { " Instructions " };
        extern int drawbox(int, int, int, int, int);

        /* fine-ruled box */
        clrw(r, c, r + BOX_Y, c + BOX_X, (WHT << 4) | GRN | BRIGHT);
        drawbox(r, c, r + BOX_Y, c + BOX_X, Vpage);

        /* centered title */
        putcur(r, c + (BOX_X - strlen(title)) / 2, Vpage);
        putstr(title, Vpage);

        /* display symbols and text using brute-force positioning */
        putcur(r + 2, c + 2, Vpage);
        put_ch(LEFTARROW, Vpage);
        put_ch(RIGHTARROW, Vpage);
        putstr("  Change selection mode", Vpage);
        putcur(r + 3, c + 2, Vpage);
        put_ch(UPARROW, Vpage);
        put_ch(DOWNARROW, Vpage);
        putstr("  Select scan lines", Vpage);
        putcur(r + 4, c + 2, Vpage);
        put_ch(L_POINT, Vpage);
        put_ch(LRC11, Vpage);
        putstr("  Set shape and exit", Vpage);
} /* end showhelp() */


/*     writestr -- write a string in the prevailing video attribute      */

static void writestr(s, pg)
char *s                 /* string to write*/
unsigned int pg;        /* screen page for writes */
{
        unsigned int r, c, cO;
        readcur(&r, &c, pg);
        for (cO = c; *s != '\0'; ++s, ++c) {
                putcur(r, c, pg);
                writec(*s, 1, pg);
        }
        /* restore cursor position */
        putcur(r, cO, pg);
} /* end writestr() */



makefile for the BIOS library
───────────────────────────────────────────────────────────────────────────

# makefile for the BIOS library
LINC = c:\include\local
LLIB = c:\lib\local

# --- inference rules ---
.c.obj:
        msc $*;

# --- objects ---
O1 = clrscrn.obj clrw.obj drawbox.obj equipchk.obj getctype.obj
        getstate.obj
O2 = kbd_stat.obj memsize.obj palette.obj putcur.obj putstr.obj put_ch.obj
O3 = readca.obj readcur.obj readdot.obj scroll.obj setctype.obj setpage.obj
O4 = setvmode.obj writea.obj writec.obj writeca.obj writedot.obj
        writetty.obj

# --- compile sources ---
clrscrn.obj:    clrscrn.c $(LINC)\bioslib.h $(LINC)\std.h

clrw.obj:      clrw.c $(LINC)\bioslib.h $(LINC)\std.h

drawbox.obj:   drawbox.c $(LINC)\video.h

equipchk.obj:  equipchk.c $(LINC)\bioslib.h $(LINC)\equip.h

getctype.obj:  getctype.c $(LINC)\bioslib.h $(LINC)\std.h

getstate.obj:  getstate.c $(LINC)\bioslib.h $(LINC)\std.h

kbd_stat.obj:  kbd_stat.c $(LINC)\bioslib.h $(LINC)\keybdlib.h

palette.obj:   palette.c $(LINC)\bioslib.h

putcur.obj:    putcur.c $(LINC)\bioslib.h $(LINC)\std.h

putstr.obj:    putstr.c $(LINC)\bioslib.h

put_ch.obj:    put_ch.c $(LINC)\bioslib.h

readca.obj:    readca.c $(LINC)\bioslib.h $(LINC)\std.h

readcur.obj:   readcur.c $(LINC)\bioslib.h $(LINC)\std.h

readdot.obj:   readdot.c $(LINC)\bioslib.h $(LINC)\std.h

scroll.obj:    scroll.c $(LINC)\bioslib.h $(LINC)\std.h

setctype.obj:  setctype.c $(LINC)\bioslib.h

setpage.obj:   setpage.c $(LINC)\bioslib.h $(LINC)\std.h $(LINC)\video.h

setvmode.obj:  setvmode.c $(LINC)\bioslib.h $(LINC)\std.h

writea.obj:    writea.c $(LINC)\std.h

writec.obj:    writec.c $(LINC)\bioslib.h $(LINC)\std.h

writeca.obj:   writeca.c $(LINC)\bioslib.h $(LINC)\std.h

writedot.obj:  writedot.c $(LINC)\bioslib.h $(LINC)\std.h

writetty.obj:  writetty.c $(LINC)\bioslib.h $(LINC)\std.h


# --- create and install the library ---
$(LLIB)\bios.lib:   $(O1) $(O2) $(O3) $(O4)
        del $(LLIB)\bios.lib
        lib $(LLIB)\bios +$(O1);
        lib $(LLIB)\bios +$(O2);
        lib $(LLIB)\bios +$(O3);
        lib $(LLIB)\bios +$(O4);
        del $(LLIB)\bios.bak



writemsg
───────────────────────────────────────────────────────────────────────────

/*   writemsg -- displays a message in a field of the prevailing
    *   video attribute and returns the number of displayable message
    *   characters written; truncates the message if its too long
    *   to fit in the field   */

#include <stdio.h>
#include <local\std.h>

int writemsg(r, c, w, s1, s2, pg)
int r, c, w ;
char *s1, *s2;
int pg;
{
        int n = 0;
        char *cp;

        /* display first part of the message */
        if (s1 != NULL)
                for (cp = s1; *cp != '\0' && n < w; ++n, ++cp) {
                        putcur(r, c + n, pg);
                        writec(*cp, 1, pg);
                }

        /* display second part of the message */
        if (s2 != NULL)
                for (cp = s2; *cp != '\0' && n < w; ++n, ++cp) {
                        putcur(r, c + n, pg);
                        writec(*cp, 1, pg);
                }

        /* pad the remainder of the field, if any, with spaces */
        if (n < w) {
                putcur(r, c + n, pg);
                writec(' ', w - n, pg);
        }
        return (n);
}



getopt()
───────────────────────────────────────────────────────────────────────────

/*
    *      Copyright (c) 1984, 1985 AT&T
    *      All Rights Reserved
    *
    *      ----- Author's Note -----
    *      getopt() is reproduced with permission of the AT&T UNIX(R) System
    *      Toolchest. This is a public domain version of getopt(3) that is
    *      distributed to registered Toolchest participants.
    *      Defining DOS_MODS alters the code slightly to obtain compatibility
    *      with DOS and support libraries provided with most DOS C compilers.
    */

#define DOS_MODS

#if defined (DOS_MODS)
        /* getopt() for DOS */
#else
#ident  "@(#)getopt.c   1.9"
#endif

/*      3.0 SID #       1.2     */
/*LINTLIBRARY*/
#define NULL    0
#define EOF     (-1)

/*
    *      For this to work under versions of DOS prior to 3.00, argv[0]
    *      must be set in main() to point to a valid program name or a
    *      reasonable substitute string.   (ARH, 10-8-86)
    */
#define ERR(s, c)       if(opterr){\
        char errbuf[2];\
        errbuf[0] = c; errbuf[1] = '\n';\
        (void) write(2, argv[0], (unsigned)strlen(argv[0]));\
        (void) write(2, s, (unsigned)strlen(s));\
        (void) write(2, errbuf, 2);}

#if defined (DOS_MODS)
/* permit function prototyping under DOS */
#include <stdlib.h>
#include <string.h>
#else
/* standard UNIX declarations */
extern int strcmp();
extern char *strchr();
/*
    *      The following line was moved here from the ERR definition
    *      to prevent a "duplicate definition" error message when the
    *      code is compiled under DOS.  (ARH, 10-8-86)
    */
extern int strlen(), write();
#endif

int     opterr = 1;
int     optind = 1;
int     optopt;
char    *optarg;

int
getopt(argc, argv, opts)
int     argc;
char    **argv, *opts;
{
        static int sp = 1;
        register int c;
        register char *cp;

        if(sp == 1)
                if(optind >= argc ||
                    argv[optind][0] != '-' || argv[optind][1] == '\0')
                        return(EOF);
                else if(strcmp(argv[optind], "--") == NULL) {
                        optind++;
                        return(EOF);
                }
        optopt = c = argv[optind][sp];
        if(c == ':' || (cp=strchr(opts, c)) == NULL) {
                ERR(": illegal option -- ", c);
                if(argv[optind][++sp] == '\0') {
                        optind++;
                        sp = 1;
                }
                return('?');
        }
        if(*++cp == ':') {
                if(argv[optind][sp+1] != '\0')
                        optarg = &argv[optind++][sp+1];
                else if(++optind >= argc) {
                        ERR(": option requires an argument -- ", c);
                        sp = 1;
                        return('?');
                } else
                        optarg = argv[optind++];
                sp = 1;
        } else {
                if(argv[optind][++sp] == '\0') {
                        sp = 1;
                        optind++;
                }
                optarg = NULL;
        }
        return(c);
}



cat
───────────────────────────────────────────────────────────────────────────

/*
    *      cat -- concatenate files
    */

#include <stdio.h>
#include <stdlib.h>
#include <local\std.h>

main(argc, argv)
int argc;
char **argv;
{
        int ch;
        char *cp;
        FILE *fp;
        BOOLEAN errflag, silent;
        static char pgm[MAXNAME + 1] = { "cat" };

        extern void getpname(char *, char *);
        extern int fcopy(FILE *, FILE *);
        extern int getopt(int, char **, char *);
        extern int optind;
        extern char *optarg;

        /* use an alias if one is given to this program */
        if (_osmajor >= 3)
                getpname(*argv, pgm);

        /* process optional arguments, if any */
        errflag = FALSE;
        silent = FALSE;
        while ((ch = getopt(argc, argv, "s")) != EOF)
                switch (ch) {
                case 's':
                        /* don't complain about nonexistent files */
                        silent = TRUE;
                        break;
                case '?':
                        /* say what? */
                        errflag = TRUE;
                        break;
                }
        if (errflag == TRUE) {
                fprintf(stderr, "Usage: %s [-s] file...\n", pgm);
                exit(1);
        }

        /* process any remaining arguments */
        argc -= optind;
        argv += optind;
        if (argc == 0)
                /* no file names -- use standard input */
                if (fcopy(stdin, stdout) != 0) {
                        fprintf(stderr, "error copying stdin");
                        exit(2);
                }
                else
                        exit(0);

        /* copy the contents of each named file to standard output */
        for (; argc-- > 0; ++argv) {
                if ((fp = fopen(*argv, "r")) == NULL) {
                        if (silent == FALSE)
                                fprintf(stderr, "%s: can't open %s\n",
                                        pgm, *argv);
                        continue;
                }
                if (fcopy(fp, stdout) != 0) {
                        fprintf(stderr, "%s: Error while copying %s",
                                pgm, *argv);
                        exit(3);
                }
                if (fclose(fp) != 0) {
                        fprintf(stderr, "%s: Error closing %s",
                                pgm, *argv);
                        exit(4);
                }
        }

        exit(0);
}



timer
───────────────────────────────────────────────────────────────────────────

/*
    *      timer -- general purpose timer program; uses the
    *      PC's intra-application communication area (ICA) as
    *      a time and date buffer
    */

#include <dos.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <memory.h>
#include <local\std.h>

#define NBYTES          16
#define ICA_SEG         0x4F
#define MAXTNUM         3
#define TIMEMASK        0x3FFFFFFF
#define FLAGBIT         0x80000000
#define IDBIT           0x40000000

main(argc, argv)
int argc;
char *argv[];
{
        int ch;
        char *cp;
        int tn;                 /* timer number */
        int errflag;            /* error flag */
        int eflag;              /* elapsed time flag */
        int sflag;              /* start timer flag */
        char dest[MAXPATH + 1]; /* destination file name */
        char timestr[MAXLINE];  /* buffer for elapsed time string */
        long now;               /* current time */
        long then;              /* previously recorded time */
        FILE *fout;
        unsigned long tdata[MAXTNUM];

        struct SREGS segregs;

        static char pgm[MAXNAME + 1] = { "timer" };

        static void usage(char *, char *);
        extern char interval(long, char *);
        extern void getpname(char *, char *);
        extern int getopt(int, char **, char *);
        extern int optind, opterr;
        extern char *optarg;

        if (_osmajor >= 3)
                getpname(*argv, pgm);

        /* process optional arguments */
        fout = stdout;
        tn = 0;
        errflag = eflag = sflag = 0;
        while ((ch = getopt(argc, argv, "0123ef:s")) != EOF) {
                switch (ch) {
                case 'e':
                        /* report elapsed timing */
                        ++eflag;
                        break;
                case 'f':
                        /* use specified log file or stream */
                        strcpy(dest, optarg);
                        if ((fout = fopen(dest, "a")) == NULL) {
                                fprintf(stderr, "%s: Cannot open %s\n",
                                        pgm, dest);
                                exit(1);
                        }
                        break;
                case 's':
                        /* start (or restart) timing an interval */
                        ++sflag;
                        break;
                case '0':
                case '1':
                case '2':
                case '3':
                        /* use specified timer */
                        tn = ch - 0x30;
                        break;
                case '?':
                        /* bad option flag */
                        ++errflag;
                        break;
                }
        }
        argc -= optind;
        argv += optind;

        /* check for errors */
        if (errflag > 0 || argc > 0)
                usage(pgm, "Bad command line option(s)");

        segread(&segregs);

        /* report current date and time */
        now = time(NULL);
        fprintf(fout, "%s", ctime(&now));

        /* control and report timer data */
        if (eflag) {
                /* report elapsed time for specified timer */
                movedata(ICA_SEG, 0, segregs.ds, tdata, NBYTES);
                then = tdata[tn];
                if ((then & FLAGBIT) != FLAGBIT || (then & IDBIT) !=
                        IDBIT) {
                        fprintf(stderr, "Timer database corrupted or not
                                set\n");
                        exit(1);
                }
                interval(now - (then & TIMEMASK), timestr);
                fprintf(stdout, "Elapsed time = %s\n", timestr);
        }
        if (sflag) {
                /* start (or restart) specified timer */
                movedata(ICA_SEG, 0, segregs.ds, tdata, NBYTES);
                tdata[tn] = (now & TIMEMASK) | FLAGBIT | IDBIT;
                movedata(segregs.ds, tdata, ICA_SEG, 0, NBYTES);
        }
        fputc('\n', fout);

        exit(0);
}

/*
    *      usage -- display a usage message and exit
    *      with an error indication
    */

static void
usage(pname, mesg)
char *pname;
char *mesg;
{
        fprintf(stderr, "%s\n", mesg);
        fprintf(stderr, "Usage: %s [-efs#]\n", pname);
        fprintf(stderr, "\t-e \tshow an elapsed time (must use start
                first)\n");
        fprintf(stderr, "\t-f file\tappend output to specified file\n");
        fprintf(stderr, "\t-s \tstart (or restart) an interval timer\n");
        fprintf(stderr, "\t-# \tselect a timer (0 to 3; default is 0)\n");
        exit(2);
}



interval
───────────────────────────────────────────────────────────────────────────

/*
    *      interval -- report the interval given in seconds as
    *      a human-readable null-terminated string
    */

#include <stdio.h>

char *
interval(seconds, buf)
long seconds;
char *buf;
{
        int hh, mm, ss;
        long remainder;

        /* calculate the values */
        hh = seconds / 3600;
        remainder = seconds % 3600;
        mm = remainder / 60;
        ss = remainder - (mm * 60);
        sprintf(buf, "%02d:%02d:%02d\0", hh, mm, ss);

        return (buf);
}



delay
───────────────────────────────────────────────────────────────────────────

/*
    *      delay -- provide a delay of ** approximately ** the
    *      specified duration (resolution is about 0.055 second)
    */

#include <local\timer.h>

void
delay(d)
float d;/* duration in seconds and fractional seconds */
{
        long ticks, then;
        extern long getticks();

        /* convert duration to number of PC clock ticks */
        ticks = d * TICKRATE;

        /* delay for the specified interval */
        then = getticks() + ticks;
        while (1)
                if (getticks() >= then)
                        break;
}



getticks
───────────────────────────────────────────────────────────────────────────

/*
    *      getticks -- get the current bios clock ticks value
    */

#include <dos.h>

#define BIOS_DATA_SEG   0x40
#define TIMER_DATA      0x6C
#define TICKS_PER_DAY   0x01800B0L

long getticks()
{
        static long total = 0;  /* accumulated count of timer ticks */
        long count;             /* current BIOS TOD count */
        long far *lp            /* far pointer */
        /* set up the far pointera to the BIOS TOD counter */
        FP_SEG(lp) = BIOS_DATA_SEG;
        FP_OFF(lp) = TIMER_DATA;
        while (1) {             /* read the TOD count */
                count = *lp
                if (*lp == count)
                        break;  /* two matching TOD readings */
        }
        /* correct for clock roll-over, if necessary */
        total = (count < total) ? count + TICKS_PER_DAY : count;
        return (total);
}



sweep
───────────────────────────────────────────────────────────────────────────

/*
    *      sweep -- produce a sound that sweeps from
    *      a low to a high frequency repeatedly until a
    *      key is pressed
    */

#include <conio.h>
#include <local\sound.h>

main()
{
        unsigned int f;
        int d, n;
        extern void setfreq(unsigned int);

        SPKR_ON;
        while (1) {
                /* give the user a way out */
                if (kbhit())
                        break;
                n = 10;
                for (f = 100; f <= 5000; f += n) {
                        setfreq(f);
                        d = 1000;
                        /* fake a short delay (machine dependent) */
                        while (d-- > 0)
                                ;
                        n += 10;
                }
        }
        SPKR_OFF;
        exit(0);
}



sound
───────────────────────────────────────────────────────────────────────────

/*
    *      sound -- produce a constant tone for a specified duration
    */

#include <conio.h>
#include <local\sound.h>

void
sound(f, dur)
unsigned int f; /* frequency of pitch in hertz */
float dur;      /* in seconds and tenths of seconds */
{
        extern void setfreq(unsigned int);
        extern void delay(float);

        /* set the frequency in hertz */
        setfreq(f);

        /* turn the speaker on for specified duration */
        SPKR_ON;
        delay(dur);
        SPKR_OFF;
}



sounds
───────────────────────────────────────────────────────────────────────────

/*
    *      sounds -- make various sounds on demand
    */

#include <stdio.h>
#include <conio.h>
#include <math.h>

#define ESC     27

extern void sound(unsigned int, float);

main()
{
        int ch;

        fprintf(stderr, "1=warble 2=error 3=confirm 4=warn\n");
        fprintf(stderr, "Esc=quit\n");
        while ((ch = getch()) != ESC)
                switch (ch) {
                case '1':
                        warble();
                        break;
                case '2':
                        error();
                        break;
                case '3':
                        confirm();
                        break;
                case '4':
                        warn();
                        break;
                }
        exit(0);
}

#define CYCLES  3
#define LOTONE  600
#define HITONE  1200
#define PERIOD  0.1

warble()
{
        int i;

        for (i = 0; i < 2 * CYCLES; ++i)
                if (i % 2)
                        sound(LOTONE, PERIOD);
                else
                        sound(HITONE, PERIOD);
}

error()
{
        float d = 0.1;

        sound(440, d);
        sound(220, d);
}

confirm()
{
        float d = 0.1;

        sound(440, d);
        sound(880, d);
}

warn()
{
        float d = 0.2;

        sound(100, d);
}



getreply
───────────────────────────────────────────────────────────────────────────

/*
    *      getreply -- display a message and wait for a reply
    */

#include <stdio.h>
#include <stdlib.h>
#include <memory.h>
#include <ctype.h>
#include <local\std.h>
#include <local\keydefs.h>
#include "linebuf.h"

char *
getreply(row, col, width, mesg, lp, size, attr, pg)
short row, col, width;  /* window location and width */
char *mesg;             /* message text */
LINEBUF *lp;            /* line pointer */
short size;             /* size of line buffer */
short attr;             /* video attribute for response field */
short pg;               /* active display page */
{
        int n, k, len;
        short mfw;      /* message field width */
        short rfw;      /* response field width */
        short ccol;     /* visible cursor column */
        int msgflag;    /* nonzero after a message is displayed */
        char *cp;       /* character pointer */
        char *wp;       /* pointer to window start */
        char *tmp;      /* temporary char pointer */

        extern int writemsg(short, short, short, char *, char *, short);

        /* display the prompt string and calculate response field width */
        putcur(row, col, pg);
        mfw = writemsg(row, col, width, mesg, NULL, pg);
        rfw = width - mfw;
        writea(attr, rfw, pg);

        /* collect the user's response */
        memset(lp->l_buf, '\0', size);
        wp = cp = lp->l_buf;
        putcur(row, col + mfw, pg);
        msgflag = 0;
        while ((k = getkey()) != K_RETURN) {
                if (msgflag) {
                        /* clear old messages */
                        errmsg("");
                        putcur(row, ccol, pg);
                        msgflag = 0;
                }
                if (isascii(k) && isprint(k)) {
                        len = strlen(cp);
                        if (cp + len - lp->l_buf < size - 1) {
                                memcpy(cp + 1, cp, len);
                                *cp = k;
                                ++cp;
                        }
                        else {
                                errmsg("input buffer full");
                                ++msgflag;
                        }
                }
                else
                        switch (k) {
                        case K_LEFT:
                                /* move left one character */
                                if (cp > lp->l_buf)
                                        --cp;
                                break;
                        case K_RIGHT:
                                /* move right one character */
                                if (*cp != '\0')
                                        ++cp;
                                break;
                        case K_UP:
                                /* pop a line off the stack */
                                if (lp->l_prev != NULL) {
                                        lp = lp->l_prev;
                                        wp = cp = lp->l_buf;
                                }
                                break;
                        case K_DOWN:
                                /* push a line onto the stack */
                                if (lp->l_next != NULL) {
                                        lp = lp->l_next;
                                        wp = cp = lp->l_buf;
                                }
                                break;
                        case K_HOME:
                                /* beginning of buffer */
                                cp = lp->l_buf;
                                break;
                        case K_END:
                                /* end of buffer */
                                while (*cp != '\0')
                                        ++cp;
                                break;
                        case K_CTRLH:
                                if (cp > lp->l_buf) {
                                        tmp = cp - 1;
                                        memcpy(tmp, cp, strlen(tmp));
                                        --cp;
                                }
                                break;
                        case K_DEL:
                                /* delete character at cursor */
                                memcpy(cp, cp + 1, strlen(cp));
                                break;
                        case K_ESC:
                                /* cancel current input */
                                lp->l_buf[0] = '\0';
                                putcur(row, col, pg);
                                writec(' ', width, pg);
                                return (NULL);
                        default:
                                errmsg("unknown command");
                                ++msgflag;
                                break;
                        }

                /* adjust the window pointer if necessary */
                if (cp < wp)
                        wp = cp;
                else if (cp >= wp + rfw)
                        wp = cp + 1 - rfw;

                /* display the reply window */
                ccol = col + mfw;
                writemsg(row, ccol, rfw, wp, NULL, pg);

                /* reposition the cursor */
                ccol = col + mfw + (cp - wp);
                putcur(row, ccol, pg);
        }
        putcur(row, col, pg);
        writec(' ', width, pg); /* blank message area */
        return (lp->l_buf);
}



tstreply
───────────────────────────────────────────────────────────────────────────

/*
    *      tstreply -- test the getreply function
    */

#include <stdio.h>
#include <string.h>
#include <local\std.h>
#include <local\video.h>
#include "linebuf.h"

#define INPUT_ROW       0
#define INPUT_COL       40
#define WIDTH           40

int Apage = 0;
BOOLEAN Silent = FALSE;

main(argc, argv)
int argc;
char *argv[];
{
        unsigned int r, c, ch, attr, revattr;
        char reply[MAXPATH + 1];
        LINEBUF buf;

        extern char *getreply(short, short, short,
                char *, LINEBUF *, short, short, short);

        /* process command line */
        if (argc == 2 && strcmp(argv[1], "-s") == 0)
                Silent = TRUE;
        else if (argc > 2) {
                fprintf(stderr, "Usage: tstreply [-s]\n");
                exit(1);
        }

        /* initial setup */
        getstate();
        readca(&ch, &attr, Apage);
        revattr = ((attr << 4) | (attr >> 4)) & 0x77;
        clrscrn(attr);
        putcur(0, 0, Apage);
        writestr("TSTREPLY", Apage);
        putcur(1, 0, Apage);
        writec(HLINE, Maxcol[Vmode} - 1, Apage);
        buf.l_buf = reply;
        buf.l_next = buf.l_prev = (LINEBUF *)NULL;

        /* demo getreply() */

        if (getreply(INPUT_ROW, INPUT_COL, WIDTH, "File: ", &buf,
                MAXPATH, revattr, 0) == NULL) {
                putcur(INPUT_ROW, INPUT_COL, Apage);
                writeca(' ', attr, WIDTH, Apage);
                putcur(2, 0, Apage);
                fprintf(stderr, "input aborted\n");
                exit(1);
        }
        putcur(INPUT_ROW, INPUT_COL, Apage);
        writeca(' ', attr, WIDTH, Apage);
        putcur(2, 0, Apage);
        fprintf(stderr, "reply = %s\n", reply);
        exit(0);
}

#define MSG_ROW 24
#define MSG_COL 0

int errmsg(mesg)
char *mesg;
{
        int n;
        extern void sound(unsigned int, float);

        putcur(MSG_ROW, MSG_COL, Apage);
        if ((n = strlen(mesg)) > 0) {
                writestr(mesg, Apage);
                if (Silent == FALSE)
                        sound(100, 0.2);
        }
        else
                writec(' ', Maxcol[Vmode] - 1 - MSG_COL, Apage);
        return (n);
}



fconfig
───────────────────────────────────────────────────────────────────────────

/*
    *      fconfig -- return a FILE pointer to a local or
    *      global configuration file, or NULL if none found
    */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <local\std.h>

FILE *
fconfig(varname, fname)
char *varname;
char *fname;
{
        FILE *fp;
        char pname[MAXPATH + 1];
        char *p;

        /* look for a local configuration file */
        if ((fp = fopen(fname, "r")) != NULL)
                return (fp);

        /* look for a directory variable */
        if ((p = getenv(strupr(varname))) != NULL) {
                strcpy(pname, p);
                strcat(pname, "\\");
                strcat(pname, fname);
                if ((fp = fopen(pname, "r")) != NULL)
                        return (fp);
        }

        /* didn't find anything to read */
        return (NULL);
}



printer.h
───────────────────────────────────────────────────────────────────────────

/*
    *      printer.h -- header for printer-control functions
    */


/* ASCII codes used for Epson MX/FX-series printer-control */
#define DC2     18      /* cancel condensed type mode */
#define DC4     20      /* cancel expanded type mode */
#define ESC     27      /* signal start of printer-control sequences */
#define FF      12      /* top of page next page */
#define SO      14      /* start expanded type mode */
#define SI      15      /* start condensed type mode */

/* font types */
#define NORMAL          0x00
#define CONDENSED       0x01
#define DOUBLE          0x02
#define EMPHASIZED      0x04
#define EXPANDED        0x08
#define ITALICS         0x10
#define UNDERLINE       0x20

/* miscellaneous constants */
#define MAXPSTR 32      /* maximum printer-control string length */

/* primary printer-control data structure */
typedef struct printer_st {
        /* hardware initialize/reset */
        char p_init[MAXPSTR];

        /* set option strings and codes */
        char p_bold[MAXPSTR];   /* bold (emphasized) on */
        char p_cmp[MAXPSTR];    /* compressed on */
        char p_ds[MAXPSTR];     /* double strike on */
        char p_exp[MAXPSTR];    /* expanded (double width) on */
        char p_ul[MAXPSTR];     /* underline on */
        char p_ital[MAXPSTR];   /* italic on */

        /* reset option strings and codes */
        char p_norm[MAXPSTR];   /* restore normal font */
        char p_xbold[MAXPSTR];  /* bold (emphasized) off */
        char p_xcmp[MAXPSTR];   /* compressed off */
        char p_xds[MAXPSTR];    /* double strike off */
        char p_xexp[MAXPSTR];   /* expanded (double width) off */
        char p_xul[MAXPSTR];    /* underline off */
        char p_xital[MAXPSTR];  /* italic off */
} PRINTER;



printer
───────────────────────────────────────────────────────────────────────────

/*
    *      printer -- interface functions for printer
    */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <local\std.h>
#include <local\printer.h>

PRINTER prt;    /* printer data */

/*
    *      setprnt -- install printer codes from configuration
    *      file for printer (defaults to Epson MX/FX series)
    */

#define NSELEM  13

int
setprnt()
{
        int n;
        char *s, line[MAXLINE];
        FILE *fp, *fconfig(char *, char *);

        /* use local or global config file, if any */
        if ((fp = fconfig("CONFIG", "printer.cnf")) != NULL) {
                n = 0;
                while (fgets(line, MAXLINE, fp) != NULL) {
                        if ((s = strtok(line, " \t\n")) == NULL)
                                return (-1);
                        switch (n) {
                        case 0:
                                strcpy(prt.p_init, s);
                                break;
                        case 1:
                                strcpy(prt.p_bold, s);
                                break;
                        case 2:
                                strcpy(prt.p_ds, s);
                                break;
                        case 3:
                                strcpy(prt.p_ital, s);
                                break;
                        case 4:
                                strcpy(prt.p_cmp, s);
                                break;
                        case 5:
                                strcpy(prt.p_exp, s);
                                break;
                        case 6:
                                strcpy(prt.p_ul, s);
                                break;
                        case 7:
                                strcpy(prt.p_xbold, s);
                                break;
                        case 8:
                                strcpy(prt.p_xds, s);
                                break;
                        case 9:
                                strcpy(prt.p_xital, s);
                                break;
                        case 10:
                                strcpy(prt.p_xcmp, s);
                                break;
                        case 11:
                                strcpy(prt.p_xexp, s);
                                break;
                        case 12:
                                strcpy(prt.p_xul, s);
                                break;
                        default:
                                /* too many lines */
                                return (-1);
                        }
                        ++n;
                }
                if (n != NSELEM)
                        /* probably not enough lines */
                        return (-1);
        }

        /* or use Epson defaults */
        strcpy(prt.p_init, "\033@");    /* hardware reset */
        strcpy(prt.p_bold, "\033E");    /* emphasized mode */
        strcpy(prt.p_ds, "\033G");      /* double-strike mode */
        strcpy(prt.p_ital, "\0334");    /* italic mode */
        strcpy(prt.p_cmp, "\017");      /* condensed mode */
        strcpy(prt.p_exp, "\016");      /* expanded mode */
        strcpy(prt.p_ul, "\033-1");     /* underline mode */
        strcpy(prt.p_xbold, "\033F");   /* cancel emphasized mode */
        strcpy(prt.p_xds, "\033H");     /* cancel double-strike mode */
        strcpy(prt.p_xital, "\0335");   /* cancel italic mode */
        strcpy(prt.p_xcmp, "\022");     /* cancel condensed mode */
        strcpy(prt.p_xexp, "\024");     /* cancel expanded mode */
        strcpy(prt.p_xul, "\033-0");    /* cancel underline mode */

        return (0);
}

/*
    *      clrprnt -- clear printer options to default values
    *      (clears individual options to avoid the "paper creep"
    *      that occurs with repeated printer resets and to avoid
    *      changing the printer's notion of top-of-form position)
    */

int
clrprnt(fout)
FILE *fout;
{
        fputs(prt.p_xbold, fout);       /* cancel emphasized mode */
        fputs(prt.p_xds, fout);         /* cancel double-strike mode */
        fputs(prt.p_xital, fout);       /* cancel italic mode */
        fputs(prt.p_xcmp, fout);        /* cancel condensed mode */
        fputs(prt.p_xexp, fout);        /* cancel expanded mode */
        fputs(prt.p_xul, fout);         /* cancel underline mode */
} /* end clrprnt() */

/*
    *      setfont -- set the printing font to the type specified
    *      by the argument (may be a compound font specification)
    */

int
setfont(ftype, fout)
int ftype;      /* font type specifier */
FILE *fout;     /* output stream */
{
        clrprnt(fout);
        if ((ftype & CONDENSED) == CONDENSED)
                if ((ftype & DOUBLE) == DOUBLE ||
                        (ftype & EMPHASIZED) == EMPHASIZED)
                        return FAILURE;
                else if (*prt.p_cmp)
                        fputs(prt.p_cmp, fout);
        if (*prt.p_ds && (ftype & DOUBLE) == DOUBLE)
                fputs(prt.p_ds, fout);
        if (*prt.p_bold && (ftype & EMPHASIZED) == EMPHASIZED)
                fputs(prt.p_bold, fout);
        if (*prt.p_exp && (ftype & EXPANDED) == EXPANDED)
                fputs(prt.p_exp, fout);
        if (*prt.p_ital && (ftype & ITALICS) == ITALICS)
                fputs(prt.p_ital, fout);
        if (*prt.p_ul && (ftype & UNDERLINE) == UNDERLINE)
                fputs(prt.p_ul, fout);

        return SUCCESS;
} /* end setfont() */



mx
───────────────────────────────────────────────────────────────────────────

/*
    *      mx -- control Epson MX-series printer
    */

#include <stdio.h>
#include <stdlib.h>
#include <local\std.h>
#include <local\printer.h>

extern PRINTER prt;     /* printer data */

main(argc, argv)
int argc;
char **argv;
{
        int ch, font;
        BOOLEAN errflag;        /* option error */
        BOOLEAN clrflag;        /* clear special fonts */
        BOOLEAN rflag;          /* hardware reset */
        BOOLEAN tflag;          /* top-of-form */
        FILE *fout;
        static char pgm[MAXNAME + 1] = { "mx" };
        extern void getpname(char *, char *);
        extern int getopt(int, char **, char *);
        extern char *optarg;
        extern int optind, opterr;
        extern int setprnt();
        extern int clrprnt(FILE *);
        extern int setfont(int, FILE *);

        if (_osmajor >= 3)
                getpname(*argv, pgm);

        if (setprnt() == -1) {
                fprintf(stderr, "%s: Bad printer configuration\n", pgm);
                exit(1);
        }

        /* interpret command line */
        errflag = clrflag = rflag = tflag = FALSE;
        font = 0;
        fout = stdprn;
        while ((ch = getopt(argc, argv, "bcdefino:prtu")) != EOF) {
                switch (ch) {
                case 'b':
                        /* set bold */
                        font |= EMPHASIZED;
                        break;
                case 'c':
                        /* set compressed */
                        font |= CONDENSED;
                        break;
                case 'd':
                        /* set double strike */
                        font |= DOUBLE;
                        break;
                case 'e':
                        /* set double strike */
                        font |= EXPANDED;
                        break;
                case 'i':
                        /* set italic */
                        font |= ITALICS;
                        break;
                case 'n':
                        /* set normal (clear all special fonts) */
                        clrflag = TRUE;
                        break;
                case 'o':
                        /* use specified output stream */
                        if ((fout = fopen(optarg, "w")) == NULL)
                                fatal(pgm, "cannot open output stream");
                        break;
                case 'p':
                        /* preview control strings on stdout */
                        fout = stdout;
                        break;
                case 'r':
                        /* hardware reset */
                        rflag = TRUE;
                        break;
                case 't':
                        /* top of form */
                        tflag = TRUE;
                        break;
                case 'u':
                        /* set underline */
                        font |= UNDERLINE;
                        break;
                case '?':
                        /* unknown option */
                        errflag = TRUE;
                        break;
                }
        }

        /* report errors, if any */
        if (errflag == TRUE || argc == 1) {
                fprintf(stderr, "Usage: %s -option\n", pgm);
                fprintf(stderr,
                        "b=bold, c=compressed, d=double strike,
                            e=expanded\n");
                fprintf(stderr,
                        "i=italic, n=normal, o file=output to file\n");
                fprintf(stderr,
                        "p=preview, r=reset, t=top-of-form,
                            u=underline\n");
                exit(2);
        }

        /* do hardware reset and formfeed first */
        if (rflag == TRUE)
                fputs(prt.p_init, fout);
        else if (tflag == TRUE)
                fputc('\f', fout);

        /* clear or set the aggregate font */
        if (clrflag == TRUE)
                clrprnt(fout);
        else if (setfont(font, fout) == FAILURE) {
                fprintf(stderr, "%s: Bad font spec\n", pgm);
                exit(3);
        }

        exit(0);
}



prtstr
───────────────────────────────────────────────────────────────────────────

/*
    *      prtstr -- send text string(s) to standard printer
    */

#include <stdio.h>
#include <stdlib.h>
#include <local\std.h>

main(argc, argv)
int argc;
char **argv;
{
        int ch;
        BOOLEAN errflag, lineflag;
        static char pgm[MAXNAME + 1] = { "prtstr" };
        FILE *fout;

        extern char *getpname(char *, char *);
        extern int getopt(int, char **, char *);
        extern int optind, opterr;
        extern char *optarg;

        if (_osmajor >= 3)
                getpname(*argv, pgm);

        errflag = FALSE;        /* process options, if any */
        lineflag = TRUE;
        fout = stdprn;
        while ((ch = getopt(argc, argv, "np")) != EOF)
                switch (ch) {
                case 'n':       /* don't emit the trailing newline */
                        lineflag = FALSE;
                        break;
                case 'p':       /* preview on stdout */
                        fout = stdout;
                        break;
                case '?':       /* bad option */
                        errflag = TRUE;
                        break;
                }
        if (errflag == TRUE) {
                fprintf(stderr, "Usage: %s [-np] [string...]\n", pgm);
                exit(1);
        }

        /* print the string(s) */
        argc -= optind;
        argv += optind;
        while (argc-- > 1 ) {
                fputs(*argv++, fout);
                fputc(' ', fout);
        }
        fputs(*argv++, fout);
        if (lineflag == TRUE)
                fputc(' ', fout);
        if (lineflag == TRUE)
                fputc('\n', fout);

        exit(0);
}



sample.bat
───────────────────────────────────────────────────────────────────────────

echo off
mx -n
prtstr This is normal text.
mx -b
prtstr This is bold (emphasized) text.
mx -n
prtstr We can mix fonts, too.
mx -be
prtstr This is bold and expanded together.
mx -n
prtstr -n We can mix fonts in a line also:
mx -b
prtstr -n " bold"
mx -i
prtstr -n " and "
mx -e
prtstr expanded.
mx -u
prtstr This is underlined text...
mx -i
prtstr ... this is italicized.
mx -n
prtstr And finally back to normal type.



select
───────────────────────────────────────────────────────────────────────────

/*
    *      select -- functions to create a selection table and
    *      to determine whether an item is an entry in the table
    */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <local\std.h>

#define NRANGES 10
#define NDIGITS 5

struct slist_st {
        long int s_min;
        long int s_max;
} Slist[NRANGES + 1];

long Highest = 0;

/*
    *      mkslist -- create the selection lookup table
    */

int
mkslist(list)
char *list;
{
        int i;
        char *listp, *s;
        long tmp;
        static long save_range();

        if (*list == '\0') {          /* fill in table of selected items */
                Slist[0].s_min = 0;   /* if no list, select all */
                Slist[0].s_max = Highest = BIGGEST;
                Slist[1].s_min = -1;
        }
        else {
                listp = list;
                for (i = 0; i < NRANGES; ++i) {
                        if ((s = strtok(listp, ", \t")) == NULL)
                                break;
                        if ((tmp = save_range(i, s)) > Highest)
                                Highest = tmp;
                        listp = NULL;
                }
                Slist[i].s_min = -1;
        }
        return (0);
} /* end mkslist() */

/*
    *      selected -- return non-zero value if the number
    *      argument is a member of the selection list
    */

int
selected(n)
long n;
{
        int i;

        /* look for converted number in selection list */
        for (i = 0; Slist[i].s_min != -1; ++i)
                if (n >= Slist[i].s_min && n <= Slist[i].s_max)
                        return (1);
        return (0);
} /* end selected() */

/*
    *      save_range -- convert a string number spec to a
    *      numeric range in the selection table and return
    *      the highest number in the range
    */

static long
save_range(n, s)
int n;
char *s;
{
        int radix = 10;
        char *cp, num[NDIGITS + 1];

        /* get the first (and possibly only) number */
        cp = num;
        while (*s != '\0' && *s != '-')
                *cp++ = *s++;
        *cp = '\0';
        Slist[n].s_min = atol(num);
        if (*s == '\0')
                /* pretty narrow range, huh? */
                return (Slist[n].s_max = Slist[n].s_min);

        /* get the second number */
        if (*++s == '\0')
                /* unspecified top end of range */
                Slist[n].s_max = BIGGEST;
        else {
                cp = num;
                while (*s != '\0')
                        *cp++ = *s++;
                *cp = '\0';
                Slist[n].s_max = atol(num);
        }
        return (Slist[n].s_max);
} /* end save_range() */



tstsel
───────────────────────────────────────────────────────────────────────────

/*
    *      tstsel -- test driver for the "select" functions
    */

#include <stdio.h>
#include <local\std.h>

#define MAXTEST 20
#define NRANGES 10
#define NDIGITS 5

extern struct slist_st {
        long int s_min;
        long int s_max;
} Slist[NRANGES + 1];

main (argc, argv)
int argc;
char **argv;
{
        int i;
        extern int mkslist(char *);
        extern int selected(unsigned int);
        static void showlist();

        if (argc != 2) {
                fprintf(stderr, "Usage: tstsel list\n");
                exit(1);
        }
        printf("argv[1] = %s\n", argv[1]);
        mkslist(argv[1]);
        showlist();
        for (i = 0; i < MAXTEST; ++i)
                printf("%2d -> %s\n", i, selected(i) ? "YES" : "NO");
        exit(0);
}

/*
    *      showlist -- display the contents of the select list
    */

static void
showlist()
{
        int i;

        /* scan the selection list and display values */
        for (i = 0; i <= NRANGES; ++i)
                printf("%2d %5ld %5ld\n", i, Slist[i].s_min,
                        Slist[i].s_max);
}



Sample output from tstsel
───────────────────────────────────────────────────────────────────────────

argv[1] = 1,2,3,5-10
    0     1     1
    1     2     2
    2     3     3
    3     5    10
    4    -1     0
    5     0     0
    6     0     0
    7     0     0
    8     0     0
    9     0     0
10     0     0
    0 -> NO
    1 -> YES
    2 -> YES
    3 -> YES
    4 -> NO
    5 -> YES
    6 -> YES
    7 -> YES
    8 -> YES
    9 -> YES
10 -> YES
11 -> NO
12 -> NO
13 -> NO
14 -> NO
15 -> NO
16 -> NO
17 -> NO
18 -> NO
19 -> NO



tabs
───────────────────────────────────────────────────────────────────────────

/*
    *      tabs -- a group of cooperating functions that set
    *      and report the settings of "tabstops"
    */

#include <local\std.h>

static char Tabstops[MAXLINE];

/*
    *      fixtabs -- set up fixed-interval tabstops
    */
void
fixtabs(interval)
register int interval;
{
        register int i;

        for (i = 0; i < MAXLINE; i++)
                Tabstops[i] = (i % interval == 0) ? 1 : 0;
} /* end fixtabs() */

/*
    *      vartabs -- set up variable tabstops from an array
    *      integers terminated by a -1 entry
    */
void
vartabs(list)
int *list;
{
        register int i;

        /* initialize the tabstop array */
        for (i = 0; i < MAXLINE; ++i)
                Tabstops[i] = 0;

        /* set user-specified tabstops */
        while (*list != -1)
                Tabstops[*++list] = 1;
} /* end vartabs() */

/*
    *      tabstop -- return non-zero if col is a tabstop
    */
int
tabstop(col)
register int col;
{
        return (col >= MAXLINE ? 1 : Tabstops[col]);
} /* end tabstop() */



showtabs
───────────────────────────────────────────────────────────────────────────

/*
    *      showtabs -- graphically display tabstop settings
    */

#include <stdio.h>
#include <stdlib.h>
#include <local\std.h>

#define MAXCOL    80
#define TABWIDTH  8

extern long Highest;

main(argc, argv)
int argc;
char *argv[];
{
        int ch, i;
        int interval, tablist[MAXLINE + 1], *p;
        char *tabstr;
        BOOLEAN errflag, fflag, vflag;
        static char pgm[MAXNAME + 1] = { "showtabs" };

        extern char *getpname(char *, char *);
        extern int getopt(int, char **, char *);
        extern char *optarg;
        extern int optind, opterr;
        extern int mkslist(char *);
        extern int selected(long);
        extern void fixtabs(int);
        extern void vartabs(int *);
        extern int tabstop(int);

        if (_osmajor >= 3)
                getpname(*argv, pgm);

        /* process command-line options */
        errflag = fflag = vflag = FALSE;
        interval = 0;
        while ((ch = getopt(argc, argv, "f:v:")) != EOF) {
                switch (ch) {
                case 'f':
                        /* used fixed tabbing interval */
                        if (vflag == FALSE) {
                                fflag = TRUE;
                                interval = atoi(optarg);
                        }
                        break;
                case 'v':
                        /* use list of tabs */
                        if (fflag == FALSE) {
                                vflag = TRUE;
                                strcpy(tabstr, optarg);
                        }
                        break;
                case '?':
                        errflag = TRUE;
                        break;
                }
        }
        if (errflag == TRUE) {
                fprintf(stderr, "Usage: %s [-f interval |
                                    -v tablist]\n", pgm);
                exit(2);
        }

        /* set the tabstops */
        if (vflag == TRUE) {
                /* user-supplied variable tab list */
                mkslist(tabstr);
                p = tablist;
                for (i = 0; i < MAXLINE && i < Highest; ++i)
                        *p++ = selected((long)i + 1) ? i : 0;
                *p = -1;        /* terminate the list */
                vartabs(tablist);
        }
        else if (fflag == TRUE)
                /* user-supplied fixed tabbing interval */
                fixtabs(interval);
        else
                /* hardware default tabbing interval */
                fixtabs(TABWIDTH);

        /* display current tabs settings */
        for (i = 0; i < MAXCOL; ++i)
                if (tabstop(i))
                        fputc('T', stdout);
                else if ((i + 1) % 10 == 0)
                        fputc('+', stdout);
                else
                        fputc('-', stdout);
        fputc('\n', stdout);

        exit(0);
}



touch
───────────────────────────────────────────────────────────────────────────

/*
    *      touch -- update modification time of file(s)
    */

#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <sys\types.h>
#include <sys\stat.h>
#include <sys\utime.h>
#include <io.h>
#include <errno.h>
#include <local\std.h>

/* error return -- big enough not to be mistaken for a bad file count */
#define ERR     0x7FFF

main(argc, argv)
int argc;
char *argv[];
{
        int ch;
        int i;
        int badcount;           /* # of files that can't be updated */
        struct stat statbuf;    /* buffer for stat results */
        BOOLEAN errflag,        /* error flag */
                cflag,          /* creation flag */
                vflag;          /* verbose flag */
        FILE *fp;

        static char pgm[MAXNAME + 1] = { "touch" };
        extern int getopt(int, char **, char *);
        extern int optind, opterr;
        extern char *optarg;
        extern void getpname(char *, char *);
        static void usage(char *);

        /* get program name from DOS (version 3.00 and later) */
        if (_osmajor >= 3)
                getpname(argv[0], pgm);

        /* process optional arguments first */
        errflag = cflag = vflag = FALSE;
        badcount = 0;
        while ((ch = getopt(argc, argv, "cv")) != EOF)
                switch (ch) {
                case 'c':
                        /* don't create files */
                        cflag = TRUE;
                        break;
                case 'v':
                        /* verbose -- report activity */
                        vflag = TRUE;
                        break;
                case '?':
                        errflag = TRUE;
                        break;
                }
        argc -= optind;
        argv += optind;

        /* check for errors including no file names */
        if (errflag == TRUE || argc <= 0) {
                usage(pgm);
                exit(ERR);
        }

        /* update modification times of files */
        for (; argc-- > 0; ++argv) {
                if (stat(*argv, &statbuf) == -1) {
                        /* file doesn't exist */
                        if (cflag == TRUE) {
                                /* don't create it */
                                ++badcount;
                                continue;
                        }
                        else if ((fp = fopen(*argv, "w")) == NULL) {
                                fprintf(stderr, "%s: Cannot create %s\n",
                                        pgm, *argv);
                                ++badcount;
                                continue;
                        }
                        else {
                                if (fclose(fp) == EOF) {
                                        perror("Error closing file");
                                        exit(ERR);
                                }
                                if (stat(*argv, &statbuf) == -1) {
                                        fprintf(stderr,
                                                "%s: Cannot stat %s\n",
                                                pgm, *argv);
                                        ++badcount;
                                        continue;
                                }
                        }
                }
                if (utime(*argv, NULL) == -1) {
                        ++badcount;
                        perror("Error updating date/time stamp");
                        continue;
                }
                if (vflag == TRUE)
                        fprintf(stderr, "Touched file %s\n", *argv);
        }

        exit(badcount);
} /* end main() */

/*
    *      usage -- display an informative usage message
    */

static void
usage(pname)
char *pname;
{
        fprintf(stderr, "Usage: %s [-cv] file ...\n", pname);
        fprintf(stderr, "\t-c  Do not create any files\n");
        fprintf(stderr, "\t-v  Verbose mode -- report activities\n");
} /* end usage() */

/*
    *      dummy functions to show how to save a little space
    */

_setenvp()
{
}

#ifndef DEBUG
_nullcheck()
{
}
#endif



tee
───────────────────────────────────────────────────────────────────────────

/*
    *      tee -- a "pipe fitter" for DOS
    */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <local\std.h>

main(argc, argv)
int argc;
char *argv[];
{
        register int ch, n;
        static char openmode[] = { "w" };
        static char pgm[MAXPATH + 1] = { "tee" };
        FILE *fp[_NFILE];       /* array of file pointers */

        extern int getopt(int, char **, char *);
        extern int optind, opterr;
        extern char *optarg;
        extern void getpname(char *, char *);

        /* check for an alias */
        if (_osmajor >= 3)
                getpname(argv[0], pgm);

        /* process command-line options, if any */
        while ((ch = getopt(argc, argv, "a")) != EOF)
                switch (ch) {
                case 'a':
                        strcpy(openmode, "a");
                        break;
                case '?':
                        break;
                }
        n = argc -= optind;
        argv += optind;

        /* check for errors */
        if (argc > _NFILE) {
                fprintf(stderr, "Too many files (max = %d)\n", _NFILE);
                exit(1);
        }

        /* open the output file(s) */
        for (n = 0; n < argc; ++n) {
                if ((fp[n] = fopen(argv[n], openmode)) == NULL) {
                        fprintf(stderr, "Cannot open %s\n", argv[n]);
                        continue;
                }
        }

        /* copy input to stdout plus opened file(s) */
        while ((ch = getchar()) != EOF) {
                putchar(ch);
                for (n = 0; n < argc; ++n)
                        if (fp[n] != NULL)
                                fputc(ch, fp[n]);
        }

        /* close file(s) */
        if (fcloseall() == -1) {
                fprintf(stderr, "Error closing a file\n");
                exit(2);
        }

        exit(0);
}



pwd
───────────────────────────────────────────────────────────────────────────

/*
    *      pwd -- print (display actually) the current directory pathname
    */

#include <stdio.h>
#include <direct.h>
#include <local\std.h>

main()
{
        char *path;

        if ((path = getcwd(NULL, MAXPATH)) == NULL) {
                perror("Error getting current directory");
                exit(1);
        }
        printf("%s\n", path);
        exit(0);
}

_setargv()
{
}

_setenvp()
{
}

_nullcheck()
{
}



rm
───────────────────────────────────────────────────────────────────────────

/*
    *      rm -- remove file(s)
    */

#include <stdio.h>
#include <stdlib.h>
#include <sys\types.h>
#include <sys\stat.h>
#include <ctype.h>
#include <io.h>
#include <local\std.h>

main(argc, argv)
int argc;
char *argv[];
{
        int ch;
        BOOLEAN errflag,
                iflag;

        static char pgm[MAXNAME + 1] = { "rm" };
        extern void getpname(char *, char *);
        static void do_rm(char *, char *, BOOLEAN);
        extern int getopt(int, char **, char *);
        extern int optind, opterr;
        extern char *optarg;

        /* get program name from DOS (version 3.00 and later) */
        if (_osmajor >= 3)
                getpname(*argv, pgm);

        /* process optional arguments first */
        errflag = iflag = FALSE;
        while ((ch = getopt(argc, argv, "i")) != EOF)
                switch (ch) {
                case 'i':
                        /* interactive -- requires confirmation */
                        iflag = TRUE;
                        break;
                case '?':
                        /* say what? */
                        errflag = TRUE;
                        break;
                }
        argc -= optind;
        argv += optind;

        if (argc <= 0 || errflag == TRUE) {
                fprintf(stderr, "%s [-i] file(s)\n", pgm);
                exit(1);
        }

        /* process remaining arguments */
        for (; argc-- > 0; ++argv)
                do_rm(pgm, *argv, iflag);

        exit(0);
} /* end main() */

/*
    *      do_rm -- remove a file
    */

static void
do_rm(pname, fname, iflag)
char *pname, *fname;
BOOLEAN iflag;
{
        int result = 0;
        struct stat statbuf;
        static BOOLEAN affirm();

        if (iflag == TRUE) {
                fprintf(stderr, "%s (y/n): ", fname);
                if (affirm() == FALSE)
                        return;
        }
        if ((result = unlink(fname)) == -1) {
                fprintf(stderr, "%s: ", pname);
                perror(fname);
        }
        return;
}

/*
    *      affirm -- return TRUE if the first character of the
    *      user's response is 'y' or FALSE otherwise
    */

#define MAXSTR  64

static BOOLEAN
affirm()
{
        char line[MAXSTR + 1];
        char *response;

        response = fgets(line, MAXSTR, stdin);
        return (tolower(*response) == 'y' ? TRUE : FALSE);
}



ls.h
───────────────────────────────────────────────────────────────────────────

/*
    *      ls.h -- header file for ls program
    */

/* structure definition for output buffer elements */
struct OUTBUF {
        unsigned short o_mode;  /* file mode (attributes) */
        long o_size;            /* file size in bytes */
        unsigned int o_date;    /* file modification date */
        unsigned int o_time;    /* file modification time */
        char *o_name;           /* DOS filespec */
};

/* constants for DOS file-matching routines */
#define FILESPEC        13      /* maximum filespec + NUL */
#define RNBYTES         21      /* bytes reserved for next_fm() calls */

/* file modes (attributes) */
#define READONLY        0x0001
#define HIDDEN          0x0002
#define SYSTEM          0x0004
#define VOLUME          0x0008
#define SUBDIR          0x0010
#define ARCHIVE         0x0020

/* structure definition for DOS file-matching routines */
struct DTA {
        unsigned char d_reserved[RNBYTES];     /* buffer for next_fm */
        unsigned char d_attr;          /* file attribute (type) byte */
        unsigned d_mtime;              /* time of last modification */
        unsigned d_mdate;              /* date of last modification */
        long d_fsize;                  /* file size in bytes */
        char d_fname[FILESPEC];        /* file spec (filename.ext + NUL) */
};



ls
───────────────────────────────────────────────────────────────────────────

/*
    *      ls -- display a directory listing
    */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <memory.h>
#include <dos.h>
#include <direct.h>
#include <signal.h>
#include <search.h>
#include <local\std.h>
#include <local\doslib.h>
#include "ls.h"

/* allocation quantities */
#define N_FILES 256
#define N_DIRS  16

/* global data */
int Multicol = 0;
int Filetype = 0;
int Hidden = 0;
int Longlist = 0;
int Reverse = 0;
int Modtime = 0;

main(argc, argv)
int argc;
char *argv[];
{
        int ch, i;
        int errflag;            /* error flag */
        char *ep;               /* environment pointer */
        int status = 0;         /* return status value */
        int fileattr;           /* file attribute number */
        struct DTA buf;         /* private disk buffer */
        char path[MAXPATH + 1]; /* working pathname */
        struct OUTBUF *fp, *fq; /* pointers to file array */
        char **dp, **dq;        /* pointer to directory pointer array */
        int fbc = 1;            /* file memory block allocation count */
        int dbc = 1;            /* directory memory block allocation */
                                /* count */
        int nfiles;             /* number of file elements */
        int ndirs;              /* number of directory elements */

        static char pgm[MAXNAME + 1] = { "ls" };

        /* function prototypes */
        void getpname(char *, char *);
        extern int getopt(int, char **, char *);
        extern int optind, opterr;
        extern char *optarg;
        extern char *drvpath(char *);
        extern void fatal(char *, char *, int);
        extern void setdta(char *);
        extern int first_fm(char *, int);
        extern int next_fm();
        extern int ls_fcomp(struct OUTBUF *, struct OUTBUF *);
        extern int ls_dcomp(char *, char *);
        extern int ls_single(struct OUTBUF *, int);
        extern int ls_multi(struct OUTBUF *, int);
        extern int ls_dirx(char *, char *);
        int bailout();

        /* guarantee that needed DOS services are available */
        if (_osmajor < 2)
                fatal(pgm, "ls requires DOS 2.00 or later", 1);

        /* get program name from DOS (version 3.00 and later) */
        if (_osmajor >= 3)
                getpname(*argv, pgm);

        /* useful aliases (DOS version 3.00 and later) */
        if (strcmp(pgm, "lc") == 0)
                ++Multicol;
        if (strcmp(pgm, "lf") == 0) {
                ++Multicol;
                ++Filetype;
        }

        /* prepare for emergencies */
        if (signal(SIGINT, bailout) == (int(*)())-1) {
                perror("Can't set SIGINT");
                exit(2);
        }

        /* process optional arguments first */
        errflag = 0;
        while ((ch = getopt(argc, argv, "aCFlrt")) != EOF)
                switch (ch) {
                case 'a':
                        /* all files (hidden, system, etc.) */
                        ++Hidden;
                        break;
                case 'C':
                        ++Multicol;
                        break;
                case 'F':
                        /* show file types (/=directory, *=executable) */
                        ++Filetype;
                        break;
                case 'l':
                        /* long list (overrides multicolumn) */
                        ++Longlist;
                        break;
                case 'r':
                        /* reverse sort */
                        ++Reverse;
                        break;
                case 't':
                        /* sort by file modification time */
                        ++Modtime;
                        break;
                case '?':
                        errflag = TRUE;
                        break;
                }
        argc -= optind;
        argv += optind;

        /* check for command-line errors */
        if (argc < 0 || errflag) {
                fprintf(stderr, "Usage: %s [-aCFlrt] [pathname ...]", pgm);
                exit(3);
        }

        /* allocate initial file and directory storage areas */
        dp = dq = (char **)malloc(N_DIRS * sizeof (char *));
        if (dp == NULL)
                fatal(pgm, "Out of memory", 4);
        fp = fq = (struct OUTBUF *)malloc(N_FILES *
                    sizeof (struct OUTBUF));
        if (fp == NULL)
                fatal(pgm, "Out of memory", 4);
        nfiles = ndirs = 0;

        /* use current directory if no args */
        if (argc == 0) {
                if (getcwd(path, MAXPATH) == NULL)
                        fatal(pgm, "Cannot get current directory", 5);
                *dq = path;
                ndirs = 1;
        }
        else {
                /* use arguments as file and directory names */
                for ( ; argc-- > 0; ++argv) {
                        strcpy(path, *argv);
                        if (path[0] == '\\') {
                                /* prepend default drive name */
                                memcpy(path + 2, path, strlen(path) + 1);
                                path[0] = 'a' + getdrive();
                                path[1] = ':';
                        }
                        if (path[1] == ':' && path[2] ==
                            '\0' && drvpath(path) == NULL) {
                                fprintf(stderr,
                                        "%s: Cannot get drive path", pgm);
                                continue;
                        }

                        /* establish private disk transfer area */
                        setdta((char *)&buf);

                        /* set file attribute for search */
                        if (Hidden)
                                fileattr = SUBDIR | HIDDEN | SYSTEM |
                                            READONLY;
                        else
                                fileattr = SUBDIR;
                        if (first_fm(path, fileattr) != 0 && path[3] !=
                            '\0') {
                                fprintf(stderr,
                                        "%s -- No such file or
                                        directory\n", path);
                                continue;
                        }
                        if ((buf.d_attr & SUBDIR) == SUBDIR || path[3] ==
                            '\0') {
                                /* path is a (sub)directory */
                                *dq = strdup(path);
                                if (++ndirs == dbc * N_DIRS) {
                                        ++dbc; /* increase space */
                                                /* requirement */
                                        dp = (char **)realloc(dp, dbc *
                                                N_DIRS
                                                * sizeof (char *));
                                        if (dp == NULL)
                                                fatal(pgm,
                                                        "Out of memory", 4);
                                        dq = dp + dbc * N_DIRS;
                                }
                                else
                                        ++dq;
                        }
                        else {
                                fq->o_name = strdup(path);
                                fq->o_mode = buf.d_attr;
                                fq->o_date = buf.d_mdate;
                                fq->o_time = buf.d_mtime;
                                fq->o_size = buf.d_fsize;
                                if (++nfiles == fbc * N_FILES) {
                                        ++fbc;
                                        fp = (struct OUTBUF *)realloc(fp,
                                                fbc * N_FILES *
                                                sizeof (struct OUTBUF));
                                        if (fp == NULL)
                                                fatal(pgm, "Out of memory",
                                                        4);
                                        fq = fp + fbc * N_FILES;
                                }
                                else
                                        ++fq;
                        }
                }
        }

        /* output file list, if any */
        if (nfiles > 0) {
                qsort(fp, nfiles, sizeof(struct OUTBUF), ls_fcomp);
                if (Longlist)
                        ls_long(fp, nfiles);
                else if (Multicol)
                        ls_multi(fp, nfiles);
                else
                        ls_single(fp, nfiles);
                putchar('\n');
        }
        free(fp);

        /* output directory lists, if any */
        if (ndirs == 1 && nfiles == 0) {
                /* expand directory and output without header */
                if (ls_dirx(pgm, *dp))
                        fprintf(stderr, "%s -- empty directory\n",
                                strlwr(*dp));
        }
        else if (ndirs > 0) {
                /* expand each directory and output with headers */
                dq = dp;
                qsort(dp, ndirs, sizeof(char *), ls_dcomp);
                while (ndirs-- > 0) {
                        fprintf(stdout, "%s:\n", strlwr(*dq));
                        if (ls_dirx(pgm, *dq++))
                                fprintf(stderr, "%s -- empty directory\n",
                                        strlwr(*dq));
                        putchar('\n');
                }
        }

        exit(0);
}

/*
    *      bailout -- optionally terminate upon interrupt
    */
int
bailout()
{
        char ch;

        signal(SIGINT, bailout);
        printf("\nTerminate directory listing? ");
        scanf("%1s", &ch);
        if (ch == 'y' || ch == 'Y')
                exit(1);
}



drvpath
───────────────────────────────────────────────────────────────────────────

/*
    *      drvpath -- convert a drive name to a full pathname
    */

#include <stdio.h>
#include <dos.h>
#include <string.h>
#include <ctype.h>
#include <local\doslib.h>

char *
drvpath(path)
char path[];    /* path string */
                /* must be large enough to hold a full DOS path + NUL */
{
        union REGS inregs, outregs;
        static int drive(char);

        /* patch root directory onto drive name */
        strcat(path, "\\");

        /* set current directory path for drive from DOS */
        inregs.h.ah = GET_CUR_DIR;
        inregs.h.dl = drive(path[0]);         /* convert to drive number */
        inregs.x.si = (unsigned)&path[3];     /* start of return string */
        intdos(&inregs, &outregs);

        return (outregs.x.cflag ? (char *)NULL : path);
}

static int
drive(dltr)
char dltr;      /* drive letter */
{
        /* 'A' (or 'a') => 1, 'B' (or 'b') => 2, etc. */
        return (tolower(dltr) - 'a' + 1);
}



ls_fcomp
───────────────────────────────────────────────────────────────────────────

/*
    *      ls_fcomp -- file and directory comparison functions
    */

#include <string.h>
#include "ls.h"

extern int Modtime;
extern int Reverse;


/*
    *      ls_fcomp -- compare two "file" items
    */

int
ls_fcomp(s1, s2)
struct OUTBUF *s1, *s2;
{
        int result;

        if (Modtime) {
                if ((result = s1->o_date - s2->o_date) == 0)
                        result = s1->o_time - s2->o_time;
        }
        else
                result = strcmp(s1->o_name, s2->o_name);

        return (Reverse ? -result : result);
} /* end_fcomp() */


/*
    *      dcomp -- compare two "directory" items
    */

int
ls_dcomp(s1, s2)
char *s1, *s2;
{
        int result;

        result = strcmp(s1, s2);

        return (Reverse ? -result : result);
} /* end ls_dcomp() */



ls_list
───────────────────────────────────────────────────────────────────────────

/*
    *      ls_list -- list functions (long, single, multi) for ls
    */

#include <stdio.h>
#include <string.h>
#include "ls.h"

#define MAXCOL          80
#define MONTH_SHIFT     5
#define MONTH_MASK      0x0F
#define DAY_MASK        0x1F
#define YEAR_SHIFT      9
#define DOS_EPOCH       80
#define HOUR_SHIFT      11
#define HOUR_MASK       0x1F
#define MINUTE_SHIFT    5
#define MINUTE_MASK     0x3F

extern int Filetype;

/*
    *      ls_long -- list items in "long" format (mode time size name)
    */

int
ls_long(buf, nelem)
struct OUTBUF *buf;
int nelem;
{
        int n = 0;
        char modebuf[5];
        static void modestr(unsigned short, char *);

        while (nelem-- > 0) {
                /* convert mode number to a string */
                modestr(buf->o_mode, modebuf);
                printf("%s ", modebuf);

                /* display file size in bytes */
                printf("%7ld ", buf->o_size);

                /* convert date and time values to formatted presentation */
                printf("%02d-%02d-%02d ", (buf->o_date >> MONTH_SHIFT) &
                        MONTH_MASK, buf->o_date & DAY_MASK, (buf->o_date >>
                        YEAR_SHIFT) + DOS_EPOCH);
                printf("%02d:%02d ", (buf->o_time >> HOUR_SHIFT) &
                        HOUR_MASK, (buf->o_time >> MINUTE_SHIFT) &
                        MINUTE_MASK);

                /* display filenames as lowercase strings */
                printf("%s\n", strlwr(buf->o_name));

                ++buf;
                ++n;
        }

        /* tell caller how many entries were printed */
        return (n);
} /* end ls_long() */


/*
    *      ls_single -- list items in a single column
    */

int
ls_single(buf, nelem)
struct OUTBUF *buf;
int nelem;
{
        int n = 0;

        while (nelem-- > 0) {
                printf("%s", strlwr(buf->o_name));
                if (Filetype && (buf->o_mode & SUBDIR) == SUBDIR)
                        putchar('\\');
                putchar('\n');
                ++buf;
                ++n;
        }

        /* tell caller how many entries were printed */
        return (n);
} /* end ls_single() */


/*
    *      ls_multi -- list items in multiple columns that
    *      vary in width and number based on longest item size
    */

int
ls_multi(buf, nelem)
struct OUTBUF *buf;
int nelem;
{
        int i, j;
        int errcount = 0;
        struct OUTBUF *tmp;     /* temporary buffer pointer */
        struct OUTBUF *base;    /* buffer pointer for multi-col output */
        int n;                  /* number of items in list */
        int len, maxlen;        /* pathname lengths */
        int ncols;              /* number of columns to output */
        int nlines;             /* number of lines to output */

        /*
            *  get length of longest pathname and calculate number
            *  of columns and lines (col width = maxlen + 1)
            */
        tmp = buf;
        n = 0;
        maxlen = 0;
        for (tmp = buf, n = 0; n < nelem; ++tmp, ++n)
                if ((len = strlen(tmp->o_name)) > maxlen)
                        maxlen = len;
        /*
            *  use width of screen - 1 to allow for newline at end of
            *  line and leave two spaces between entries (one for optional
            *  file type flag)
            */
        ncols = (MAXCOL - 1) / (maxlen + 2);
        nlines = n / ncols;
        if (n % ncols)
                ++nlines;

        /* output multi-column list */
        base = buf;
        for (i = 0; i < nlines; ++i) {
                tmp = base;
                for (j = 0; j < ncols; ++j) {
                        len = maxlen + 2;
                        len -= printf("%s", strlwr(tmp->o_name));
                        if (Filetype && (tmp->o_mode & SUBDIR) == SUBDIR) {
                                putchar('\\');
                                --len;
                        }
                        while (len-- > 0)
                                putchar(' ');
                        tmp += nlines;
                        if (tmp - buf >= nelem)
                                break;
                }
                putchar('\n');
                ++base;
        }

        return (errcount);
} /* end ls_multi() */


static void
modestr(mode, s)
unsigned short mode;    /* file mode number */
char s[];               /* mode string buffer */
{

        /* fill in the mode string to show what's set */
        s[0] = (mode & SUBDIR) == SUBDIR ? 'd' : '-';
        s[1] = (mode & HIDDEN) == HIDDEN ? 'h' : '-';
        s[2] = (mode & SYSTEM) == SYSTEM ? 's' : '-';
        s[3] = (mode & READONLY) == READONLY ? 'r' : '-';
        s[4] = '\0';
} /* end modestr() */



ls_dirx
───────────────────────────────────────────────────────────────────────────

/*
    *      ls_dirx -- expand the contents of a directory using
    *      the DOS first/next matching file functions
    */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <malloc.h>
#include <dos.h>
#include <direct.h>
#include <signal.h>
#include <search.h>
#include <local\std.h>
#include <local\doslib.h>
#include "ls.h"

#define NFILES  1024

extern int Recursive;
extern int Longlist;
extern int Multicol;
extern int Hidden;

int
ls_dirx(pname, namep)
char *pname;
char *namep;
{
        int status = 0;                 /* function return value */
        int n;                          /* number of items found */
        int fileattr;                   /* attributes of file-matching */
        struct DTA buf;                 /* disk transfer area */
        struct OUTBUF *bp, *bq;         /* output buffer pointers */
        char path[MAXPATH + 1];         /* working path string */

        extern void setdta(char *);
        extern int first_fm(char *, int);
        extern int next_fm();
        extern int ls_fcomp(struct OUTBUF *, struct OUTBUF *);
        extern char last_ch(char *);

        /* allocate a buffer */
        bp = bq = (struct OUTBUF *)malloc(NFILES * sizeof(struct OUTBUF));
        if (bp == NULL)
                fatal(pname, "Out of memory");

        /* form name for directory search */
        strcpy(path, namep);
        if (last_ch(path) != '\\')
                strcat(path, "\\");
        strcat(path, "*.*");

        /* list the files found */
        n = 0;
        /* establish a private DTA */
        setdta((char *)&buf);
        /* select file attributes */
        if (Hidden)
                fileattr = SUBDIR | HIDDEN | SYSTEM | READONLY;
        else
                fileattr = SUBDIR;
        if (first_fm(path, fileattr) == 0) {
                /* add file or directory to the buffer */
                do {
                        if (!Hidden && buf.d_fname[0] == '.')
                                continue;
                        bq->o_name = strdup(buf.d_fname);
                        bq->o_mode = buf.d_attr;
                        bq->o_size = buf.d_fsize;
                        bq->o_date = buf.d_mdate;
                        bq->o_time = buf.d_mtime;
                        ++bq;
                        ++n;
                        setdta((char *)&buf);   /* reset to our DTA */
                } while (next_fm() == 0);

                if (n > 0) {
                        /* got some -- sort and list them */
                        qsort(bp, n, sizeof(struct OUTBUF), ls_fcomp);
                        if (Longlist)
                                ls_long(bp, n);
                        else if (Multicol)
                                ls_multi(bp, n);
                        else
                                ls_single(bp, n);
                }
        }
        else
                ++status;
        free(bp);

        return (status);
}



first_fm
───────────────────────────────────────────────────────────────────────────

/*
    *      first_fm - find first file match in work directory
    */

#include <dos.h>
#include <local\doslib.h>

int
first_fm(path, fa)
char *path;     /* pathname of directory */
int fa;         /* attribute(s) of file to match */
{
        union REGS inregs, outregs;

        /* find first matching file */
        inregs.h.ah = FIND_FIRST;
        inregs.x.cx = fa;
        inregs.x.dx = (unsigned int)path;
        (void)intdos(&inregs, &outregs);

        return (outregs.x.cflag);
}

/*
    *      next_fm - find next file match in work directory
    */

#include <dos.h>
#include <local\doslib.h>

int
next_fm()
{
        union REGS inregs, outregs;

        /* find next matching file */
        inregs.h.ah = FIND_NEXT;
        (void)intdos(&inregs, &outregs);

        return (outregs.x.cflag);
}



makefile for the LS program
───────────────────────────────────────────────────────────────────────────

# makefile for the LS program

LIB=c:\lib
LLIB=c:\lib\local

first_fm.obj:   first_fm.c
        msc $*;
        lib $(LLIB)\dos -+ $*;

next_fm.obj:    next_fm.c
        msc $*;
        lib $(LLIB)\dos -+ $*;

setdta.obj:     setdta.c
        msc $*;
        lib $(LLIB)\dos -+ $*;

getdrive.obj:   getdrive.c
        msc $*;
        lib $(LLIB)\dos -+ $*;

drvpath.obj:    drvpath.c
        msc $*;
        lib $(LLIB)\dos -+ $*;

ls_fcomp.obj:   ls_fcomp.c ls.h
        msc $*;

ls_dirx.obj:    ls_dirx.c ls.h
        msc $*;

ls_list.obj:    ls_list.c ls.h
        msc $*;

ls.obj:         ls.c ls.h
        msc $*;

ls.exe:         ls.obj ls_dirx.obj ls_fcomp.obj ls_list.obj \
                $(LLIB)\util.lib $(LLIB)\dos.lib
        link $* ls_dirx ls_fcomp ls_list $(LIB)\ssetargv, $*,, $(LLIB)\util
            $(LLIB)\dos;



pr_help
───────────────────────────────────────────────────────────────────────────

/*
    *      pr_help -- display an abbreviated manual page
    */

#include <stdio.h>
#include <local\std.h>

void
pr_help(pname)
char *pname;
{
        static char *m_str[] = {
            "The following options may be used singly or in "combination:",
            "-e\t set Epson-compatible mode",
            "-f\t use formfeed to eject a page (default is newlines)",
            "-g\t use generic printer mode",
            "-h hdr\t use specified header instead of filename",
            "-l len\t set page length in lines (default = 66)",
            "-n\t enable line-numbering (default = off)",
            "-o cols\t offset from left edge in columns (default = 5)",
            "-p\t preview output on screen (may be redirected)",
            "-s list\t print only selected pages",
            "-w cols\t line width in columns (default = 80)"
        };
        int i, n = sizeof (m_str)/ sizeof (char *);

        fprintf(stderr, "Usage: %s [options] file...\n\n", pname);
        for (i = 0; i < n; ++i)
            fprintf(stderr, "%s\n", m_str[i]);

        return;
}



print.h
───────────────────────────────────────────────────────────────────────────

/*
    *      print.h -- header information for print programs
    */

/* default printing format information */
#define BOTTOM  3       /* blank lines at bottom of page */
#define MARGIN  5       /* default margin width in columns */
#define MAXPCOL 80      /* maximum number of printed columns per line */
#define MAXPSTR 64      /* maximum characters in a string variable */
#define PAGELEN 66      /* default page length (at 6 lines per inch) */
#define LPI     6       /* default lines per inch */
#define TABSPEC 8       /* default tab separation */
#define TOP1    2       /* blank lines above header line */
#define TOP2    2       /* blank lines below header line */

/* primary data structure for printer programs */
typedef struct pr_st {
        /* numeric variables */
        int p_top1;     /* lines above header */
        int p_top2;     /* lines below header */
        int p_btm;      /* lines in footer */
        int p_wid;      /* width in columns */
        int p_lmarg;    /* left margin */
        int p_rmarg;    /* right margin */
        int p_len;      /* lines per page */
        int p_lpi;      /* lines per inch */
        int p_lnum;     /* nonzero turns line-numbering on */
        int p_mode;     /* zero for generic printer */
        int p_font;     /* font number when in nongeneric mode */
        int p_ff;       /* nonzero uses formfeed to eject page */
        int p_tabint;   /* tab interval */

        /* string variables */
        char p_hdr[MAXPSTR];
        char p_dest[MAXPSTR];
} PRINT;



pr
───────────────────────────────────────────────────────────────────────────

/*
    *      pr -- file printer
    */

#include <stdio.h>
#include <stdlib.h>
#include <dos.h>
#include <local\std.h>
#include "print.h"

char Pagelist[MAXLINE];

main(argc, argv)
int argc;
char **argv;
{
        int ch;
        BOOLEAN errflag;
        extern PRINT pcnf;
        static char pgm[MAXNAME + 1] = { "pr" };

        extern char getpname(char *, char *);
        extern int getopt(int, char **, char *);
        extern char *optarg;
        extern int optind, opterr;
        extern int pr_gcnf(char *);
        extern pr_file(char *, int, char **);
        extern void pr_help(char *);
        extern void fixtabs(int);
        extern int setprnt();

        if (_osmajor >= 3)
                getpname(*argv, pgm);

        /* do configuration */
        if (pr_gcnf(pgm) != 0) {
                fprintf(stderr, "%s: Configuration error", pgm);
                exit(2);
        }
        if (setprnt() == -1) {
                fprintf(stderr, "%s: Bad printer configuration\n", pgm);
                exit(1);
        }
        fixtabs(pcnf.p_tabint);

        /* process command-line arguments */
        while ((ch = getopt(argc, argv, "efgh:l:no:ps:w:")) != EOF) {
                switch (ch) {
                case 'e':
                        /* force "Epson-compatible " printer mode */
                        pcnf.p_mode = 1;
                        break;
                case 'f':
                        /* use formfeed to eject a page */
                        pcnf.p_ff = 1;
                        break;
                case 'g':
                        /* force "generic" printer mode */
                        pcnf.p_mode = 0;
                        break;
                case 'h':
                        /* use specified header */
                        strcpy(pcnf.p_hdr, optarg);
                        break;
                case 'l':
                        /* set lines per page */
                        pcnf.p_len = atoi(optarg);
                        break;
                case 'n':
                        /* enable line-numbering */
                        pcnf.p_lnum = 1;
                        break;
                case 'o':
                        /* set left margin */
                        pcnf.p_lmarg = atoi(optarg);
                        break;
                case 'p':
                        /* preview output on screen */
                        strcpy(pcnf.p_dest, "");
                        break;
                case 's':
                        /* output selected pages */
                        strcpy(Pagelist, optarg);
                        break;
                case 'w':
                        /* set page width in columns */
                        pcnf.p_wid = atoi(optarg);
                        break;
                case '?':
                        /* unknown option */
                        errflag = TRUE;
                        break;
                }
        }
        if (errflag == TRUE) {
                pr_help(pgm);
                exit(3);
        }

        /* print the files */
        pr_file(pgm, argc - optind, argv += optind);

        exit(0);
}



pr_gcnf
───────────────────────────────────────────────────────────────────────────

/*
    *      pr_gcnf -- get configuration for pr program
    */

#include <stdio.h>
#include <string.h>
#include <local\std.h>
#include <local\printer.h>
#include "print.h"

/* expected number of configuration items */
#define N_NBR   12

PRINT pcnf;

int
pr_gcnf(pname)
char *pname;
{
        char line[MAXLINE];
        char *s;
        int cnf[N_NBR];
        int n, errcount, good;
        FILE *fp, *fconfig(char *, char *);

        /* get configuration file values, if any */
        n = good = errcount = 0;
        if ((fp = fconfig("CONFIG", "pr.cnf")) != NULL) {
                while (n < N_NBR && (s = fgets(line, MAXLINE, fp)) != NULL) {
                        cnf[n] = atoi(s);
                        ++n;
                }
                if ((s = fgets(line, MAXLINE, fp)) == NULL)
                        ++errcount;
                else
                        strcpy(pcnf.p_dest, strtok(line, " \t\n"));
                if (n != N_NBR)
                        ++errcount;
                if (errcount == 0)
                        good = 1;
                if (fclose(fp) == -1)
                        fatal(pname, "cannot close config file", 1);
        }

        /* use config data if good; use defaults otherwise */
        pcnf.p_top1 = good ? cnf[0]: TOP1;
        pcnf.p_top2 = good ? cnf[1] : TOP2;
        pcnf.p_btm = good ? cnf[2] : BOTTOM;
        pcnf.p_wid = good ? cnf[3] : MAXPCOL;
        pcnf.p_lmarg = good ? cnf[4] : MARGIN;
        pcnf.p_rmarg = good ? cnf[5] : MARGIN;
        pcnf.p_len = good ? cnf[6] : PAGELEN;
        pcnf.p_lpi = good ? cnf[7] : LPI;
        pcnf.p_mode = good ? cnf[8] : 0;
        pcnf.p_lnum = good ? cnf[9] : 0;
        pcnf.p_ff = good ? cnf[10] : 0;
        pcnf.p_tabint = good ? cnf[11] : TABSPEC;
        if (!good)
                strcpy(pcnf.p_dest, "PRN");
        if (pcnf.p_mode == 1)
                pcnf.p_font = CONDENSED;
        strcpy(pcnf.p_hdr, "");

        return (errcount);
}



pr_file
───────────────────────────────────────────────────────────────────────────

/*
    *      pr_file -- process each filename or standard input
    */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <local\std.h>
#include "print.h"

int
pr_file(pname, ac, av)
char *pname;
int ac;
char **av;
{
        int ch, errcount = 0;
        FILE *fin, *fout;
        extern PRINT pcnf;

        extern void fatal(char*, char*, int);
        extern int pr_cpy(FILE *, FILE *, char *);

        /* open output stream only if not already open */
        if (*pcnf.p_dest == '\0' || strcmp(pcnf.p_dest, "CON") == 0)
                fout = stdout;
        else if (strcmp(pcnf.p_dest, "PRN") == 0)
                fout = stdprn;
        else if (strcmp(pcnf.p_dest, "AUX") == 0)
                fout = stdaux;
        else
                if ((fout = fopen(pcnf.p_dest, "w")) == NULL)
                        fatal(pname, "Error open destination stream", 1);

        /* prepare input stream */
        if (ac == 0)
                pr_cpy(stdin, fout, "");
        else {
                for (; ac > 0; --ac, ++av) {
                        if ((fin = fopen(*av, "r")) == NULL) {
                                fprintf(stderr, "%s: Error opening %s\n",
                                        pname, *av);
                                continue;
                        }
                        if (pr_cpy(fin, fout, *av) == -1) {
                                fprintf(stderr, "%s: Cannot stat %s",
                                        pname, *av);
                                continue;
                        }
                        if (fclose(fin) == EOF)
                                fatal(pname, "Error closing input file", 1);
                }
        }

        return (errcount);
}



pr_cpy
───────────────────────────────────────────────────────────────────────────

/*
    *      pr_cpy -- copy input stream to output stream
    */

#include <stdio.h>
#include <string.h>
#include <local\std.h>
#include <local\printer.h>
#include <sys\types.h>
#include <sys\stat.h>
#include <time.h>
#include "print.h"

extern PRINT pcnf;
extern char Pagelist[MAXLINE];
extern long Highest;

int
pr_cpy(fin, fout, fname)
FILE *fin;
FILE *fout;
char *fname;
{
        int errcount = 0;
        unsigned int p_line;    /* page-relative line number */
        long f_line;            /* file-relative line number */
        long f_page;            /* file-relative page number */
        int lnlen;              /* line length */
        char line[MAXLINE];     /* input line buffer */
        struct stat tbuf;       /* file information */
        long ltime;             /* date and time */
        FILE *fnull, *fx;       /* additional output file pointers */

        extern void mkslist(char *);    /* make a selection list */
        extern int selected(long);      /* is item in the list? */
        extern int spaces(int, FILE *); /* emit string of spaces */
        extern int setfont(int, FILE *);/* set printer font type */
        extern int clrprnt(FILE *);     /* clear special fonts */
        extern int lines(int, FILE *);  /* emit string of blank lines */
        static int fit(int, int);       /* will line fit on page? */
        extern int pr_line(char *, FILE *, unsigned int);

        /* install page selection list, if any */
        if (Pagelist[0] != '\0') {
                /* open the NUL device for dumping output */
                if ((fnull = fopen("NUL", "w")) == NULL) {
                        perror("Error opening NUL device");
                        exit(1);
                }
                mkslist(Pagelist);
        }
        else
                Highest = BIGGEST;
        /* get date and time stamp */
        if (*fname == '\0')
                /* using stdin -- use today's date and time */
                ltime = time(NULL);
        else {
                if (stat(fname, &tbuf) == -1)
                        return (-1);
                /* use file's modification time */
                ltime = tbuf.st_mtime;
        }
        p_line = 0;
        f_line = 1;
        f_page = 1;
        while ((lnlen = pr_getln(line, MAXLINE, fin)) > 0 ) {
                /* if formfeed or no room for line, eject page */
                if (line[0] == '\f' || !fit(lnlen, p_line)) {
                        /* to top of next page */
                        if (pcnf.p_ff == 0)
                                lines(pcnf.p_len - p_line, fx);
                        else
                                fputc('\f', fx);
                        p_line = 0;
                }

                /* if at top of page, print the header */
                if (p_line == 0) {
                        if (f_page > Highest)
                                break;
                        fx = selected(f_page) ? fout : fnull;
                        p_line += lines(pcnf.p_top1, fx);
                        if (pcnf.p_mode != 0)
                                setfont(EMPHASIZED, fx);
                        spaces(pcnf.p_lmarg, fx);
                        if (*pcnf.p_hdr != '\0')
                                fprintf(fx, "%s  ", pcnf.p_hdr);
                        else if (*fname != '\0')
                                fprintf(fx, "%s  ", strupr(fname));
                        fprintf(fx, "Page %u  ", f_page++);
                        fputs(ctime(&ltime), fx);
                        ++p_line;
                        if (pcnf.p_mode != 0)
                                setfont(pcnf.p_font, fx);
                        p_line += lines(pcnf.p_top2, fx);
                }

                /* OK to output the line */
                if (line[0] != '\f')
                        p_line += pr_line(line, fx, f_line++);
        }
        if (ferror(fin) != 0)
                ++errcount;
        if (p_line > 0 && p_line < pcnf.p_len)
                if (pcnf.p_ff == 0)
                        lines(pcnf.p_len - p_line, fx);
                else
                        fputc('\f', fx);

        if (pcnf.p_mode != 0)
                clrprnt(fx);
        return (errcount);
}

/*
    *      fit -- return nonzero value if enough physical
    *      lines are available on the current page to take
    *      the current logical line of text
    */

#define NFLDWIDTH  8    /* width of number field */

static int
fit(len, ln)
int len, ln;
{
        int need, left; /* physical lines */
        int cols;       /* columns of actual output */
        int lw;         /* displayable line width */

        /* total need (columns -> physical lines) */
        cols = len + (pcnf.p_lnum > 0 ? NFLDWIDTH : 0);
        lw = pcnf.p_wid - pcnf.p_lmarg - pcnf.p_rmarg;
        need = 1 + cols / lw;

        /* lines remaining on page */
        left = pcnf.p_len - ln - pcnf.p_btm;

        return (need <= left ? 1 : 0);
}



pr_getln
───────────────────────────────────────────────────────────────────────────

/*
    *      pr_getln -- get a line of text while expanding tabs;
    *      put text into an array and return the length of the line
    *      including termination to the calling function.
    */

#include <stdio.h>
#include <stdlib.h>
#include <local\std.h>

int
pr_getln(s, lim, fin)
char *s;
int lim;
FILE *fin;
{
        int ch;
        register char *cp;

        extern int tabstop();   /* query tabstop array */

        cp = s;
        while (--lim > 0 && (ch = fgetc(fin)) != EOF && ch != '\n' && ch !=
                '\f') {
                if (ch == '\t')
                        /* loop and store spaces until next tabstop */
                        do
                                *cp++ = ' ';
                        while (--lim > 0 && tabstop(cp - s) == 0);
                else
                        *cp++ = ch;
        }
        if (ch == EOF && cp - s == 0)
                ;
        else if (ch == EOF || ch == '\n')
                *cp++ = '\n';   /* assure correct line termination */
        else if (ch == '\f' && cp - s == 0) {
                *cp++ = '\f';
                fgetc(fin);     /* toss the trailing newline */
        }
        *cp = '\0';

        return (cp - s);
}



pr_line
───────────────────────────────────────────────────────────────────────────

/*
    *      pr_line -- ouput a buffered logical line and
    *      return a count of physical lines produced
    */

#include <stdio.h>
#include <stdlib.h>
#include <local\std.h>
#include "print.h"

int
pr_line(s, fout, rline)
char *s;                /* buffered line of text */
FILE *fout;             /* output stream */
unsigned int rline;     /* file-relative line number */
{
        int c_cnt;      /* character position in output line */
        int nlines;     /* number of lines output */
        extern PRINT pcnf;

        extern int spaces(int, FILE *); /* emit string of spaces */

        nlines = 1;
        c_cnt = 0;

        /* output the left indentation, if any */
        c_cnt += spaces(pcnf.p_lmarg, fout);

        /* output the line number if numbering enabled */
        if (pcnf.p_lnum != 0)
                c_cnt += fprintf(fout, "%6u  ", rline);

        /* output the text of the line */
        while (*s != '\0') {
                if (c_cnt > (pcnf.p_wid - pcnf.p_rmarg)) {
                        fputc('\n', fout);
                        ++nlines;
                        c_cnt = 0;
                        c_cnt = spaces(pcnf.p_lmarg, fout);
                }
                fputc(*s, fout);
                ++s;
                ++c_cnt;
        }

        return (nlines);
}



makefile for the pr program
───────────────────────────────────────────────────────────────────────────

# makefile for the pr program

LINC=c:\include\local
LIB=c:\lib
LLIB=c:\lib\local

pr_cpy.obj:     pr_cpy.c print.h $(LINC)\printer.h
        msc  $*;
        lib prlib -+$*;

pr_file.obj:    pr_file.c print.h
        msc  $*;
        lib prlib -+$*;

pr_getln.obj:   pr_getln.c
        msc  $*;
        lib prlib -+$*;

pr_help.obj:    pr_help.c
        msc  $*;
        lib prlib -+$*;

pr_gcnf.obj:    pr_gcnf.c print.h $(LINC)\printer.h
        msc  $*;
        lib prlib -+$*;

pr_line.obj:    pr_line.c print.h
        msc  $*;
        lib prlib -+$*;

pr.obj:         pr.c print.h
        msc  $*;

pr.exe:         pr.obj prlib.lib $(LLIB)\util.lib
        link $* $(LIB)\ssetargv, $*,, prlib $(LLIB)\util;



hex.c
───────────────────────────────────────────────────────────────────────────

/*
    *      hex.c -- hex conversions routines
    */

#define NIBBLE  0x000F
#define BYTE    0x00FF
#define WORD    0xFFFF

char hextab[] = {
        '0', '1', '2', '3', '4', '5', '6', '7',
        '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'
};

/*
    *      byte2hex -- convert a byte to a string
    *      representation of its hexadecimal value
    */

char *
byte2hex(data, buf)
unsigned char data;
char *buf;
{
        char *cp;
        unsigned int d;

        d = data & BYTE;
        cp = buf;
        *cp++ = hextab[(d >> 4) & NIBBLE];
        *cp++ = hextab[d & NIBBLE];
        *cp = '\0';

        return (buf);
}

/*
    *      word2hex -- convert a word to a string
    *      representation of its hexadecimal value
    */

char *
word2hex(data, buf)
unsigned int data;
char *buf;
{
        char *cp;
        unsigned int d;

        d = data & WORD;
        cp = buf;
        *cp++ = hextab[(d >> 12) & NIBBLE];
        *cp++ = hextab[(d >> 8) & NIBBLE];
        *cp++ = hextab[(d >> 4) & NIBBLE];
        *cp++ = hextab[d & NIBBLE];
        *cp = '\0';

        return (buf);
}



dump
───────────────────────────────────────────────────────────────────────────

/*
    *      dump -- display contents of non-ASCII files in hex byte and
    *      ASCII character forms (like the DOS debug dump option)
    */

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys\types.h>
#include <sys\stat.h>
#include <io.h>
#include <local\std.h>

#define STDINPUT        0
#define LINEWIDTH       80

main(argc,argv)
int argc;
char *argv[];
{
        int ch;
        BOOLEAN sflag = FALSE,
                vflag = FALSE,
                errflag = FALSE;
        int fd;
        static char pgm[MAXNAME + 1] = { "dump" };

        extern int getopt(int, char **, char *);
        extern char *optarg;
        extern int optind, opterr;
        extern void getpname(char *, char *);
        extern int hexdump(int, BOOLEAN);
        extern void fatal(char *, char *, int);

        if (_osmajor >= 3)
                getpname(*argv, pgm);

        while ((ch = getopt(argc, argv, "sv")) != EOF)
                switch (ch) {
                case 's': /* strip -- convert all non-ASCII to '.' */
                        sflag = TRUE;
                        break;
                case 'v': /* verbose -- tell user what's happening */
                        vflag = TRUE;
                        break;
                case '?': /* bad option */
                        errflag = TRUE;
                        break;
                }

        if (errflag == TRUE) {
                fprintf(stderr, "Usage: %s [-sv] [file...]\n", pgm);
                exit(1);
        }

        if (optind == argc) {
                if (setmode(STDINPUT, O_BINARY) == -1)
                        fatal(pgm, "Cannot set binary mode", 2);
                hexdump(STDINPUT, sflag);
                exit(0);
        }

        for ( ; optind < argc; ++optind) {
                if ((fd = open(argv[optind], O_BINARY | O_RDONLY)) == -1) {
                        fprintf(stderr,
                                "%s: Error opening %s -- ", pgm,
                                argv[optind]);
                        perror("");
                        continue;
                }
                if (vflag == TRUE)
                        fprintf(stdout, "\n%s:\n", argv[optind]);
                if (hexdump(fd, sflag) == FAILURE) {
                        fprintf(stderr,
                                "%s: Error reading %s -- ", pgm,
                                    argv[optind]);
                        perror("");
                }
                if (close(fd) == -1)
                        fatal(pgm, "Error closing input file", 3);
        }

        exit(0);
}



hexdump
───────────────────────────────────────────────────────────────────────────

/*
    *      hexdump -- read data from an open file and "dump"
    *      it in side-by-side hex and ASCII to standard output
    */

#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <local\std.h>

#define LINEWIDTH       80
#define NBYTES          16
#define WORD            0xFFFF
#define RGHTMARK        179
#define LEFTMARK        179
#define DEL             0x7F

int hexdump(fd, strip)
int fd;
BOOLEAN strip;
{
        unsigned char i;
        int n;                  /* bytes per read operation */
        unsigned long offset;   /* bytes from start of file */
        char inbuf[BUFSIZ + 1], outbuf[LINEWIDTH + 1];
        char hexbuf[5];
        register char *inp, *outp;

        extern char *byte2hex(unsigned char, char *);
        extern char *word2hex(unsigned int, char *);

        offset = 0;
        while ((n = read(fd, inbuf, BUFSIZ)) != 0) {
                if (n == -1)
                        return FAILURE;
                inp = inbuf;
                while (inp < inbuf + n) {
                        outp = outbuf;

                        /* offset in hex */
                        outp += sprintf(outp, "%08lX",
                                offset + (unsigned long)(inp - inbuf));
                        *outp++ = ' ';

                        /* block of bytes in hex */
                        for (i = 0; i < NBYTES; ++i) {
                                *outp++ = ' ';
                                strcpy(outp, byte2hex(*inp++, hexbuf));
                                outp += 2;
                        }
                        *outp++ = ' ';
                        *outp++ = ' ';
                        *outp++ = (strip == TRUE) ? '|' : LEFTMARK;

                        /* same block of bytes in ASCII */
                        inp -= NBYTES;
                        for (i = 0; i < NBYTES; ++i) {
                                if (strip == TRUE && (*inp < ' ' || *inp >=
                                    DEL))
                                        *outp = '.';
                                else if (iscntrl(*inp))
                                        *outp = '.';
                                else
                                        *outp = *inp;
                                ++inp;
                                ++outp;
                        }
                        *outp++ = (strip == TRUE) ? '|' : RGHTMARK;
                        *outp++ = '\n';
                        *outp = '\0';
                        fputs(outbuf, stdout);
                }
                offset += n;
        }
        return SUCCESS;
}



show
───────────────────────────────────────────────────────────────────────────

/*
    *      show -- a filter that displays the contents of a file
    *      in a way that is guaranteed to be displayable
    */

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <io.h>
#include <local\std.h>

main(argc, argv)
int argc;
char **argv;
{
        int ch;
        FILE *fp;
        BOOLEAN sflag = FALSE;  /* strip non-ASCII characters */
        BOOLEAN vflag = FALSE;  /* verbose mode */
        BOOLEAN wflag = FALSE;  /* filter typical word-processing codes */
        BOOLEAN errflag = FALSE;
        static char pgm[] = { "show" };

        extern int getopt(int, char **, char *);
        extern char *optarg;
        extern int optind, opterr;
        extern int showit(FILE *, BOOLEAN, BOOLEAN);
        extern void fatal(char *, char *, int);

        if (_osmajor >= 3)
                getpname(*argv, pgm);

        while ((ch = getopt(argc, argv, "svw")) != EOF) {
                switch (ch) {
                case 's': /* strip non-ASCII characters */
                        sflag = TRUE;
                        break;
                case 'v': /* verbose */
                        vflag = TRUE;
                        break;
                case 'w': /* use word-processing conventions */
                        wflag = sflag = TRUE;
                        break;
                case '?':
                        errflag = TRUE;
                        break;
                }
        }

        /* check for errors */
        if (errflag == TRUE) {
                fprintf(stderr, "Usage: %s [-sv] [file...]\n", pgm);
                exit(1);
        }

        /* if no file names, use standard input */
        if (optind == argc) {
                if (setmode(fileno(stdin), O_BINARY) == -1)
                        fatal(pgm, "Cannot set binary mode on stdin", 2);
                showit(stdin, sflag, wflag);
                exit(0);
        }

        /* otherwise, process remainder of command line */
        for ( ; optind < argc; ++optind) {
                if ((fp = fopen(argv[optind], "rb")) == NULL) {
                        fprintf(stderr,
                                "%s: Error opening %s\n", pgm,
                                argv[optind]);
                        perror("");
                        continue;
                }
                if (vflag == TRUE)
                        fprintf(stdout, "\n%s:\n", argv[optind]);
                if (showit(fp, sflag, wflag) != 0) {
                        fprintf(stderr,
                                "%s: Error reading %s\n", pgm,
                                argv[optind]);
                        perror("");
                }
                if (fclose(fp) == EOF)
                        fatal(pgm, "Error closing input file", 2);
        }

        exit(0);
}



showit
───────────────────────────────────────────────────────────────────────────

/*
    *      showit -- make non-printable characters in
    *      the stream fp visible (or optionally strip them)
    */

#include <stdio.h>
#include <ctype.h>
#include <local\std.h>

int
showit(fp, strip, wp)
FILE *fp;
BOOLEAN strip;  /* strip non-ASCII codes */
BOOLEAN wp;     /* filter typical word-processing codes */
{
        int ch;

        clearerr(fp);
        while ((ch = getc(fp)) != EOF)
                if (isascii(ch) && (isprint(ch) || isspace(ch)))
                        switch (ch) {
                        case '\r':
                                if (wp == TRUE) {
                                        if ((ch = getc(fp)) != '\n')
                                                ungetc(ch, fp);
                                        putchar('\r');
                                        putchar('\n');
                                }
                                else
                                        putchar(ch);
                                break;
                        default:
                                putchar(ch);
                                break;
                        }
                else if (strip == FALSE)
                        printf("\\%02X", ch);
                else if (wp == TRUE && isprint(ch & ASCII))
                        putchar(ch & ASCII);
        return (ferror(fp));
}



dspytype
───────────────────────────────────────────────────────────────────────────

/*
    *      dspytype -- determine display adapter type
    */

#include <stdio.h>
#include <dos.h>
#include <local\bioslib.h>
#include <local\video.h>

#define MDA_SEG 0xB000
#define CGA_SEG 0xB800

main()
{
        extern int memchk(unsigned int, unsigned int);
        int mdaflag, egaflag, cgaflag;
        int ega_mem, ega_mode;
        unsigned int features, switches;
        static int memtab[] = {
                64, 128, 192, 256
        };

        mdaflag = egaflag = cgaflag = 0;

        /* look for display adapters */
        if (ega_info(&ega_mem, &ega_mode, &features, &switches))
                ++egaflag;
        fputs("Enhanced graphics adapter ", stdout);
        if (egaflag) {
                fputs("installed\n", stdout);
                fprintf(stdout, "EGA memory size = %d-KB\n",
                        memtab[ega_mem]);
                fprintf(stdout, "EGA is in %s mode\n",
                        ega_mode ? "monochrome" : "color");
        }
        else
                fputs("not installed\n", stdout);

        /* look for IBM monochrome memory */
        if (!egaflag || (egaflag && ega_mode == 0))
                if (memchk(MDA_SEG, 0))
                        ++mdaflag;

        /* look for CGA memory */
        if (!egaflag || (ega && ega_mode == 1))
                if (memchk(CGA_SEG, 0))
                        ++cgaflag;
        }
        fputs("Monochrome adapter ", stdout);
        if (mdaflag)
                fputs("installed\n", stdout);
        else
                fputs("not installed\n", stdout);
        fputs("Color/graphics adapter ", stdout);
        if (cgaflag)
                fputs("installed\n", stdout);
        else
                fputs("not installed\n", stdout);

        /* report video settings */
        getstate();
        fprintf(stdout, "mode=%d width=%d page=%d\n", Vmode, Vwidth,
                Vpage);

        exit(0);
}



memchk
───────────────────────────────────────────────────────────────────────────

/*
    *      memchk -- look for random-access memory at
    *      a specified location; return nonzero if found
    */

#include <dos.h>
#include <memory.h>

int
memchk(seg, os)
unsigned int seg;
unsigned int os;
{
        unsigned char tstval, oldval, newval;
        unsigned int ds;
        struct SREGS segregs;

        /* get value of current data segment */
        segread(&segregs);
        ds = segregs.ds;

        /* save current contents of test location */
        movedata(seg, os, ds, &oldval, 1);

        /* copy a known value into test location */
        tstval = 0xFC;
        movedata(ds, &tstval, seg, os, 1);

        /* read test value back and compare to value written */
        movedata(seg, os, ds, &newval, 1);
        if (newval != tstval)
                return (0);

        /* restore original contents of test location */
        movedata(ds, &oldval, seg, os, 1);

        return (1);
}



ega_info
───────────────────────────────────────────────────────────────────────────

/*
    *      ega_info -- gather information about an EGA;
    *      return a nonzero value if one is found
    */

#include <dos.h>
#include <local\bioslib.h>

#define EGA_INFO 0x10
#define NMODES  2
#define NMEMSIZ 4

int
ega_info(memsize, mode, features, switches)
int *memsize;           /* EGA memory size indicator: 0 = 64K */
                        /* 1 = 128K; 2 = 192K; 3 = 256K */
int *mode;              /* 0 = color mode; 1 = mono mode */
                        /* use getstate function to find out which mode */
unsigned int
        *features,      /* feature bit settings */
        *switches;      /* EGA switch settings */
{
        int result = 0;
        union REGS inregs, outregs;

        /* request EGA information */
        inregs.h.ah = ALT_FUNCTION;
        inregs.h.bl = EGA_INFO;
        int86(VIDEO_IO, &inregs, &outregs);

        *memsize = outregs.h.bl;
        *mode = outregs.h.bh;
        *features = outregs.h.ch;
        *switches = outregs.h.cl;

        /* return nonzero if EGA installed */
        if (*memsize >= 0 && *memsize < NMEMSIZ &&
                *mode >= 0 && *mode < NMODES)
                result = 1;
        return (result);
}



cpblk
───────────────────────────────────────────────────────────────────────────

/*
    *      cpblk -- copy a block of characters and attributes
    *      while eliminating "snow" on a standard CGA display
    */

#include <conio.h>
#include <memory.h>

#define BLKCNT  10
#define VSTAT   0x3DA
#define VRBIT   8
#define WRDCNT  200
#define NBYTES  (2 * WRDCNT)

/* macro to synchronize with vertical retrace period */
#define VSYNC   while ((inp(VSTAT) & VRBIT) == VRBIT); \
                while ((inp(VSTAT) & VRBIT) != VRBIT)

int
cpblk(src_os, src_seg, dest_os, dest_seg)
unsigned int src_os, src_seg, dest_os, dest_seg;
{
        register int i;
        int n;
        register int delta;

        n = 0;
        delta = 0;
        for (i = 0; i < BLKCNT ; ++i) {
                /* copy a block of words during vertical retrace */
                VSYNC;
                movedata(src_seg, src_os + delta,
                        dest_seg, dest_os + delta, NBYTES);
                n += WRDCNT;

                /* adjust buffer offset */
                delta += NBYTES;
        }

        return (n);
}



st
───────────────────────────────────────────────────────────────────────────

/*
    *      st -- screen test using cpblk function
    */

#include <stdio.h>
#include <stdlib.h>
#include <conio.h>
#include <ctype.h>
#include <dos.h>
#include <local\std.h>
#include <local\video.h>

#define CG_SEG          0xB800
#define MONO_SEG        0xB000
#define NBYTES          0x1000
#define PAGESIZ         (NBYTES / 2)
#define PG0_OS          0
#define PG1_OS          PG0_OS + NBYTES
#define ESC             27
#define MAXSCAN         14

main(argc, argv)
int argc;
char *argv[];
{
        int i;
        int k;                          /* user command character */
        int ca;                         /* character/attribute word */
        int ch;                         /* character value read */
        int row, col;                   /* cursor position upon entry */
        int c_start, c_end;             /* cursor scan lines */
        unsigned char attr;             /* saved video attribute */
        unsigned dseg;                  /* destination buffer segment */
        unsigned os;                    /* page offset in bytes */
        static unsigned sbuf[NBYTES];   /* screen buffer */
        unsigned *bp;                   /* screen element pointer */
        unsigned sseg;                  /* source segment */
        int special;                    /* use special copy routine */
        int apg, vpg;                   /* active and visual display
                                        /* pages */

        /* segment register values */
        struct SREGS segregs;

        extern void swap_int(int *, int *);

        static char pgm[] = { "st" };   /* program name */

        /* save user's current video state */
        getstate();
        readcur(&row, &col, Vpage);
        putcur(row - 1, 0, Vpage);
        readca(&ch, &attr, Vpage);
        getctype(&c_start, &c_end, Vpage);
        setctype(MAXSCAN, c_end);
        clrscrn(attr);
        putcur(1, 1, Vpage);

        /* initialize destination segment */
        special = 1;
        fputs("ScreenTest (ST): ", stderr);
        if (Vmode == CGA_C80 || Vmode == CGA_M80) {
                dseg = CG_SEG;
                fprintf(stderr, "Using CGA mode %d", Vmode);
        }
        else if (Vmode == MDA_M80) {
                dseg = MONO_SEG;
                fprintf(stderr, "Using MDA (mode %d)", Vmode);
                special = 0;
        } else
                fprintf(stderr, "%s: Requires 80-column text mode\n", pgm);

        /* process command-line arguments */
        if (argc > 2) {
                fprintf(stderr, "Usage: %s [x]\n", pgm);
                exit(2);
        }
        else if (argc == 2)
                special = 0;    /* bypass special block move */

        /* get data segment value */
        segread(&segregs);
        sseg = segregs.ds;

        /* set up "active" and "visual" display pages */
        apg = 1;        /* page being written to */
        vpg = 0;        /* page being viewed */

        /* display buffers on the user's command */
        fprintf(stderr, " -- Type printable text; Esc=exit");
        while ((k = getkey()) != ESC) {
                if (isascii(k) && isprint(k)) {
                        /* fill the buffer */
                        ca = ((k % 0xEF) << 8) | k;
                        for (bp = sbuf; bp - sbuf < PAGESIZ; ++bp)
                                *bp = ca;
                        if (Vmode == MDA_M80)
                                os = 0;
                        else
                                os = (apg == 0) ? PG0_OS : PG1_OS;
                        if (special)
                                cpblk(sbuf, sseg, os, dseg);
                        else
                                movedata(sseg, sbuf, dseg, os, NBYTES);
                        if (Vmode != MDA_M80) {
                                swap_int(&apg, &vpg);
                                setpage(vpg);
                        }
                }
                else {
                        clrscrn(attr);
                        putcur(0, 0, Vpage);
                        writestr(" Type printable text; Esc = exit ", vpg);
                }
        }

        /* restore user's video conditions and return to DOS */
        setpage(Vpage);
        clrscrn(attr);
        putcur(0, 0, Vpage);
        setctype(c_start, c_end);
        exit(0);
}



sbuf.h
───────────────────────────────────────────────────────────────────────────

/*
    *      sbuf.h -- header file for buffered screen interface
    */

#define SB_OK   0
#define SB_ERR  (-1)

/* screen-buffer constants */
#define SB_ROWS 25
#define SB_COLS 80
#define SB_SIZ  SB_ROWS * SB_COLS

/* screen character/attribute buffer element definition */
struct BYTEBUF {
        unsigned char ch;       /* character */
        unsigned char attr;     /* attribute */
};

union CELL {
        struct BYTEBUF b;
        unsigned short cap;     /* character/attribute pair */
};

/* screen-buffer control structure */
struct BUFFER {
        /* current position */
        short row, col;

        /* pointer to screen-buffer array */
        union CELL *bp;

        /* changed region per screen-buffer row */
        short lcol[SB_ROWS];    /* left end of changed region */
        short rcol[SB_ROWS];    /* right end of changed region */

        /* buffer status */
        unsigned short flags;
};

/* buffer flags values */
#define SB_DELTA        0x01
#define SB_RAW          0x02
#define SB_DIRECT       0x04
#define SB_SCROLL       0x08

/* coordinates of a window (rectangular region) on the screen buffer */
struct REGION {
        /* current position */
        short row, col;

        /* window boundaries */
        short r0, c0;   /* upper left corner */
        short r1, c1;   /* lower right corner */

        /* scrolling region boundaries */
        short sr0, sc0; /* upper left corner */
        short sr1, sc1; /* lower right corner */

        /* window buffer flags */
        unsigned short wflags;
};



sb_init
───────────────────────────────────────────────────────────────────────────

/*
    *      sb_init -- initialize the buffered screen interface
    */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <local\sbuf.h>

/* global data declarations */
struct BUFFER Sbuf;                     /* control information */
union CELL Scrnbuf[SB_ROWS][SB_COLS];   /* screen-buffer array */

int
sb_init()
{
        int i;
        char *um;       /* update mode */

        /* set initial parameter values */
        Sbuf.bp = &Scrnbuf[0][0];
        Sbuf.row = Sbuf.col = 0;
        for (i = 0; i < SB_ROWS; ++i) {
                Sbuf.lcol[i] = SB_COLS;
                Sbuf.rcol[i] = 0;
        }
        Sbuf.flags = 0;

        /* set screen update mode */
        um = strupr(getenv("UPDATEMODE"));
        if (um == NULL || strcmp(um, "BIOS") == 0)
                Sbuf.flags &= ~SB_DIRECT;
        else if (strcmp(um, "DIRECT") == 0)
                Sbuf.flags |= SB_DIRECT;
        else
                return SB_ERR;
        return SB_OK;
}



sb_show
───────────────────────────────────────────────────────────────────────────

/*
    *      sb_show -- copy the screen buffer to display memory
    */

#include <stdio.h>
#include <dos.h>
#include <memory.h>
#include <local\sbuf.h>
#include <local\video.h>

#define MDA_SEG 0xB000
#define CGA_SEG 0xB800
#define NBYTES  (2 * SB_COLS)

/* macro to synchronize with vertical retrace period */
#define VSTAT   0x3DA
#define VRBIT   8
#define VSYNC   while ((inp(VSTAT) & VRBIT) == VRBIT); \
                while ((inp(VSTAT) & VRBIT) != VRBIT)

extern struct BUFFER Sbuf;
extern union CELL Scrnbuf[SB_ROWS][SB_COLS];

int
sb_show(pg)
short pg;
{
        register short r, c;
        short n;
        short count, ncols;
        unsigned int src_os, dest_os;
        struct SREGS segregs;

        if ((Sbuf.flags & SB_DIRECT) == SB_DIRECT) {
                /* use the direct-screen interface */
                segread(&segregs);

                /* determine extent of changes */
                n = 0;
                for (r = 0; r < SB_ROWS; ++r)
                        if (Sbuf.lcol[r] <= Sbuf.rcol[r])
                                ++n;
                src_os = dest_os = 0;
                if (n <= 2)
                        /* copy only rows that contain changes */
                        for (r = 0; r < SB_ROWS; ++r) {
                                if (Sbuf.lcol[r] <= Sbuf.rcol[r]) {
                                        /* copy blocks during vertical
                                            * retrace */
                                        VSYNC;
                                        movedata(segregs.ds, &Scrnbuf[0][0]
                                                    + src_os,
                                                CGA_SEG, dest_os, NBYTES);
                                        Sbuf.lcol[r] = SB_COLS;
                                        Sbuf.rcol[r] = 0;
                                }
                                src_os += NBYTES;
                                dest_os += NBYTES;
                        }
                else {
                        /* copy the entire buffer */
                        count = 3 * NBYTES;
                        ncols = 3 * NBYTES;
                        for (r = 0; r < SB_ROWS - 1; r += 3) {
                                VSYNC;
                                movedata(segregs.ds, &Scrnbuf[0][0]
                                            +src_os,
                                        CGA_SEG, dest_os, count);
                                src_os += ncols;
                                dest_os += count;
                        }
                        VSYNC;
                        movedata(segregs.ds, &Scrnbuf[0][0] + src_os,
                                CGA_SEG, dest_os, NBYTES);
                        for (r = 0; r < SB_ROWS; ++r) {
                                Sbuf.lcol[r] = SB_COLS;
                                Sbuf.rcol[r] = 0;
                        }
                }
        }
        else
                /* use the BIOS video interface */
                for (r = 0; r < SB_ROWS; ++r)
                        /* copy only changed portions of lines */
                        if (Sbuf.lcol[r] < SB_COLS && Sbuf.rcol[r] > 0) {
                                for (c = Sbuf.lcol[r]; c <= Sbuf.rcol[r];
                                        ++c) {
                                        putcur(r, c, pg);
                                        writeca(Scrnbuf[r][c].b.ch,
                                                Scrnbuf[r][c].b.attr, 1,
                                                    pg);
                                }
                                Sbuf.lcol[r] = SB_COLS;
                                Sbuf.rcol[r] = 0;
                        }

        /* the display now matches the buffer -- clear flag bit */
        Sbuf.flags &= ~SB_DELTA;

        return SB_OK;
}



sb_new
───────────────────────────────────────────────────────────────────────────

/*
    *      sb_new -- prepare a new window (rectangular region)
    *      and return a pointer to it
    */

#include <stdio.h>
#include <malloc.h>
#include <local\sbuf.h>

struct REGION *
sb_new(top, left, height, width)
int top;        /* top row */
int left;       /* left column */
int height;     /* total rows */
int width;      /* total columns */
{
        struct REGION *new;

        /* allocate the data-control structure */
        new = (struct REGION *)malloc(sizeof (struct REGION));
        if (new != NULL) {
                new->r0 = new->sr0 = top;
                new->r1 = new->sr1 = top + height - 1;
                new->c0 = new->sc0 = left;
                new->c1 = new->sc1 = left + width - 1;
                new->row = new->col = 0;
                new->wflags = 0;
        }
        return (new);
}



sb_move
───────────────────────────────────────────────────────────────────────────

/*
    *      sb_move -- move the screen-buffer "cursor"
    */

#include <local\sbuf.h>

extern struct BUFFER Sbuf;
extern union CELL Scrnbuf[SB_ROWS][SB_COLS];

int
sb_move(win, r, c)
struct REGION *win;     /* window pointer */
register short r, c;    /* buffer row and column */
{
        /* don't change anything if request is out of range */
        if (r < 0 || r > win->r1 - win->r0 || c < 0 || c > win->c1 -
            win->c0)
                return SB_ERR;
        win->row = r;
        win->col = c;
        Sbuf.row = r + win->r0;
        Sbuf.col = c + win->c0;
        return SB_OK;
}



sb_fill
───────────────────────────────────────────────────────────────────────────

/*
    *      sb_fill -- fill region routines
    */

#include <local\sbuf.h>

extern struct BUFFER Sbuf;
extern union CELL Scrnbuf[SB_ROWS][SB_COLS];

/*
    *      sb_fill -- set all cells in a specified region
    *      to the same character/attribute value
    */

int
sb_fill(win, ch, attr)
struct REGION *win;
unsigned char ch;       /* fill character */
unsigned char attr;     /* fill attribute */
{
        register int i, j;
        unsigned short ca;

        ca = (attr << 8) | ch;
        for (i = win->sr0; i <= win->sr1; ++i) {
                for (j = win->sc0; j <= win->sc1; ++j)
                        Scrnbuf[i][j].cap = ca;
                if (win->sc0 < Sbuf.lcol[i])
                        Sbuf.lcol[i] = win->sc0;
                if (win->sc1 > Sbuf.rcol[i])
                        Sbuf.rcol[i] = win->sc1;
        }
        Sbuf.flags |= SB_DELTA;

        return SB_OK;
}


/*
    *      sb_fillc -- set all cells in a specified region
    *      to the same character value; leave attributes undisturbed
    */

int
sb_fillc(win, ch)
struct REGION *win;
unsigned char ch;       /* fill character */
{
        register int i, j;

        for (i = win->sr0; i <= win->sr1; ++i) {
                for (j = win->sc0; j <= win->sc1; ++j)
                        Scrnbuf[i][j].b.ch = ch;
                if (win->sc0 < Sbuf.lcol[i])
                        Sbuf.lcol[i] = win->sc0;
                if (win->sc1 > Sbuf.rcol[i])
                        Sbuf.rcol[i] = win->sc1;
        }
        Sbuf.flags |= SB_DELTA;

        return SB_OK;
}


/*
    *      sb_filla -- set all cells in a specified region
    *      to the same attribute value; leave characters undisturbed
    */

int
sb_filla(win, attr)
struct REGION *win;
unsigned char attr;     /* fill attribute */
{
        register int i, j;

        for (i = win->sr0; i <= win->sr1; ++i) {
                for (j = win->sc0; j <= win->sc1; ++j)
                        Scrnbuf[i][j].b.attr = attr;
                if (win->sc0 < Sbuf.lcol[i])
                        Sbuf.lcol[i] = win->sc0;
                if (win->sc1 > Sbuf.rcol[i])
                        Sbuf.rcol[i] = win->sc1;
        }
        Sbuf.flags |= SB_DELTA;

        return SB_OK;
}



sb_put
───────────────────────────────────────────────────────────────────────────

    *      sb_put -- routines to put characters and strings into the
    *      screen buffer; the cursor location is altered
    */

#include <local\sbuf.h>
#include <ctype.h>

extern struct BUFFER Sbuf;
extern union CELL Scrnbuf[SB_ROWS][SB_COLS];

/*
    *      sb_putc -- put a character into a screen-buffer window
    */

int
sb_putc(win, ch)
struct REGION *win;
unsigned char ch;
{
        short cmax, rmax;
        short lim;
        short noscroll = 0, puterr = 0;

        /* calculate screen-buffer position and limits */
        cmax = win->c1 - win->c0;
        rmax = win->r1 - win->r0;
        Sbuf.row = win->r0 + win->row;
        Sbuf.col = win->c0 + win->col;

        /* process the character */
        switch (ch) {
        case '\b':
                /* non-destructive backspace */
                if (win->col > 0) {
                        --win->col;
                        Sbuf.col = win->c0 + win->col;
                        return SB_OK;
                }
                else
                        return SB_ERR;
        case '\n':
                /* clear trailing line segment */
                while (win->col < cmax)
                        if (sb_putc(win, ' ') == SB_ERR)
                                ++puterr;
                break;
        case '\t':
                /* convert tab to required number of spaces */
                lim = win->col + 8 - (win->col & 0x7);
                while (win->col < lim)
                        if (sb_putc(win, ' ') == SB_ERR)
                                ++puterr;
                break;
        default:
                /* if printable ASCII, place the character in the buffer */
                if (isascii(ch) && isprint(ch))
                        Scrnbuf[Sbuf.row][Sbuf.col].b.ch = ch;
                if (Sbuf.col < Sbuf.lcol[Sbuf.row])
                        Sbuf.lcol[Sbuf.row] = Sbuf.col;
                if (Sbuf.col > Sbuf.rcol[Sbuf.row])
                        Sbuf.rcol[Sbuf.row] = Sbuf.col;
                break;
        }

        /* update the cursor position */
        if (win->col < cmax)
                ++win->col;
        else if (win->row < rmax) {
                win->col = 0;
                ++win->row;
        }
        else if ((win->wflags & SB_SCROLL) == SB_SCROLL) {
                sb_scrl(win, 1);
                win->col = 0;
                win->row = rmax;
        }
        else
                ++noscroll;

        /* update screen-buffer position */
        Sbuf.row = win->r0 + win->row;
        Sbuf.col = win->c0 + win->col;
        Sbuf.flags |= SB_DELTA;

        return ((noscroll || puterr) ? SB_ERR : SB_OK);
} /* end sb_putc() */


/*
    *      sb_puts -- put a string into the screen buffer
    */

int
sb_puts(win, s)
struct REGION *win;
unsigned char *s;
{
        while (*s)
                if (sb_putc(win, *s++) == SB_ERR)
                        return SB_ERR;
        return SB_OK;
} /* end sb_puts() */



sb_read
───────────────────────────────────────────────────────────────────────────

/*
    *      sb_read -- read character/attribute data
    */

#include <local\sbuf.h>

extern struct BUFFER Sbuf;
extern union CELL Scrnbuf[SB_ROWS][SB_COLS];

unsigned char
sb_ra(win)
struct REGION *win;     /* window pointer */
{
        return (Scrnbuf[win->r0 + win->row][win->c0 + win->col].b.attr);
} /* end sb_ra() */


/*
    *      sb_rc -- read character from current location in screen buffer
    */

unsigned char
sb_rc(win)
struct REGION *win;     /* window pointer */
{
        return (Scrnbuf[win->r0 + win->row][win->c0 + win->col].b.ch);
} /* end sb_rc() */


/*
    *      sb_rca -- read character/attribute pair from current
    *      location in screen buffer
    */

unsigned short
sb_rca(win)
struct REGION *win;     /* window pointer */
{
        return (Scrnbuf[win->r0 + win->row][win->c0 + win->col].cap);
} /* end sb_rca() */



sb_scrl
───────────────────────────────────────────────────────────────────────────

/*
    *      sb_scrl -- scrolling routines
    */

#include <local\sbuf.h>

extern struct BUFFER Sbuf;
extern union CELL Scrnbuf[SB_ROWS][SB_COLS];


/*
    *      sb_scrl -- scroll the specified window
    *      n lines (direction indicated by sign)
    */

int
sb_scrl(win, n)
struct REGION *win;
short n;                /* number of rows to scroll */
{
        register short r, c;

        if (n == 0)
                /* clear the entire region to spaces */
                sb_fillc(win, ' ');
        else if (n > 0) {
                /* scroll n rows up */
                for (r = win->sr0; r <= win->sr1 - n; ++r) {
                        for (c = win->sc0; c <= win->sc1; ++c)
                                Scrnbuf[r][c] = Scrnbuf[r + n][c];
                        if (win->sc0 < Sbuf.lcol[r])
                                Sbuf.lcol[r] = win->sc0;
                        if (win->sc1 > Sbuf.rcol[r])
                                Sbuf.rcol[r] = win->sc1;
                }
                for ( ; r <= win->sr1; ++r) {
                        for (c = win->sc0; c <= win->sc1; ++c)
                                Scrnbuf[r][c].b.ch = ' ';
                        if (win->sc0 < Sbuf.lcol[r])
                                Sbuf.lcol[r] = win->sc0;
                        if (win->sc1 > Sbuf.rcol[r])
                                Sbuf.rcol[r] = win->sc1;
                }
        }
        else {
                /* scroll n rows down */
                n = -n;
                for (r = win->sr1; r >= win->sr0 + n; --r) {
                        for (c = win->sc0; c <= win->sc1; ++c)
                                Scrnbuf[r][c] = Scrnbuf[r - n][c];
                        if (win->sc0 < Sbuf.lcol[r])
                                Sbuf.lcol[r] = win->sc0;
                        if (win->sc1 > Sbuf.rcol[r])
                                Sbuf.rcol[r] = win->sc1;
                }
                for ( ; r >= win->sr0; --r) {
                        for (c = win->sc0; c <= win->sc1; ++c)
                                Scrnbuf[r][c].b.ch = ' ';
                        if (win->sc0 < Sbuf.lcol[r])
                                Sbuf.lcol[r] = win->sc0;
                        if (win->sc1 > Sbuf.rcol[r])
                                Sbuf.rcol[r] = win->sc1;
                }
        }
        Sbuf.flags |= SB_DELTA;
        return SB_OK;
} /* end sb_scrl() */


/*
    *      sb_set_scrl -- set the scroll region boundaries
    */

int
sb_set_scrl(win, top, left, bottom, right)
struct REGION *win;     /* window pointer */
short top, left;        /* upper left corner */
short bottom, right;    /* lower left corner */
{
        if (top < 0 || left < 0 ||
                bottom > win->r1 - win->r0 || right > win->c1 - win->c0)
                return SB_ERR;
        win->sr0 = win->r0 + top;
        win->sc0 = win->c0 + left;
        win->sr1 = win->r0 + bottom - 1;
        win->sc1 = win->c0 + right - 1;
        return SB_OK;
} /* end sb_set_scrl() */



sb_write
───────────────────────────────────────────────────────────────────────────

/*
    *      sb_write -- screen-buffer write routines
    */

#include <local\sbuf.h>

extern struct BUFFER Sbuf;
extern union CELL Scrnbuf[SB_ROWS][SB_COLS];

/*
    *      sb_wa -- write an attribute to a region of the screen buffer
    */

int
sb_wa(win, attr, n)
struct REGION *win;     /* window pointer */
unsigned char attr;     /* attribute */
short n;                /* repetition count */
{
        short i;
        short row;
        short col;

        i = n;
        row = win->r0 + win->row;
        col = win->c0 + win->col;
        while (i--)
                Scrnbuf[row][col + i].b.attr = attr;

        /* marked the changed region */
        if (col < Sbuf.lcol[row])
                Sbuf.lcol[row] = col;
        if (col + n > Sbuf.rcol[row])
                Sbuf.rcol[row] = col + n;
        Sbuf.flags |= SB_DELTA;

        return (i == 0) ? SB_OK : SB_ERR;
} /* end sb_wa() */


/*
    *      sb_wc -- write a character to a region of the screen buffer
    */

int
sb_wc(win, ch, n)
struct REGION *win;     /* window pointer */
unsigned char ch;       /* character */
short n;                /* repetition count */
{
        short i;
        short row;
        short col;
        i = n;
        row = win->r0 + win->row;
        col = win->c0 + win->col;
        while (i--)
                Scrnbuf[row][col + i].b.ch = ch;

        /* marked the changed region */
        if (col < Sbuf.lcol[row])
                Sbuf.lcol[row] = col;
        if (col + n > Sbuf.rcol[row])
                Sbuf.rcol[row] = col + n;
        Sbuf.flags |= SB_DELTA;

        return (i == 0 ? SB_OK : SB_ERR);
} /* end sb_wc() */


/*
    *      sb_wca -- write a character/attribute pair to a region
    *      of the screen buffer
    */

int
sb_wca(win, ch, attr, n)
struct REGION *win;     /* window pointer */
unsigned char ch;       /* character */
unsigned char attr;     /* attribute */
short n;                /* repetition count */
{
        int i;
        short row;
        short col;

        i = n;
        row = win->r0 + win->row;
        col = win->c0 + win->col;
        while (i--)
                Scrnbuf[row][col + i].cap = (attr << 8) | ch;

        /* marked the changed region */
        if (col < Sbuf.lcol[row])
                Sbuf.lcol[row] = col;
        if (col + n > Sbuf.rcol[row])
                Sbuf.rcol[row] = col + n;
        Sbuf.flags |= SB_DELTA;

        return (i == 0 ? SB_OK : SB_ERR);
} /* end sb_wca() */



sb_box
───────────────────────────────────────────────────────────────────────────

/*
    *      sb_box -- draw a box around the perimeter of a window
    *      using the appropriate IBM graphics characters
    */

#include <local\sbuf.h>
#include <local\video.h>
#include <local\box.h>

int
sb_box(win, type, attr)
struct REGION *win;
short type;
unsigned char attr;
{
        register short r;     /* row index */
        short x;              /* interior horizontal line length */
        short maxr, maxc;     /* maximum row and col values */
        BOXTYPE *boxp;        /* pointer to box-drawing character struct */
        static BOXTYPE box[] = {
                '+',   '+',   '+',   '+',   '-',   '-',   '|',   '|',
                ULC11, URC11, LLC11, LRC11, HBAR1, HBAR1, VBAR1, VBAR1,
                ULC22, URC22, LLC22, LRC22, HBAR2, HBAR2, VBAR2, VBAR2,
                ULC12, URC12, LLC12, LRC12, HBAR1, HBAR1, VBAR2, VBAR2,
                ULC21, URC21, LLC21, LRC21, HBAR2, HBAR2, VBAR1, VBAR1,
                BLOCK, BLOCK, BLOCK, BLOCK, HBART, HBARB, BLOCK, BLOCK
        };

        boxp = &box[type];
        maxc = win->c1 - win->c0;
        maxr = win->r1 - win->r0;
        x = maxc - 1;

        /* draw top row */
        sb_move(win, 0, 0);
        sb_wca(win, boxp->ul, attr, 1);
        sb_move(win, 0, 1);
        sb_wca(win, boxp->tbar, attr, x);
        sb_move(win, 0, maxc);
        sb_wca(win, boxp->ur, attr, 1);

        /* draw left and right sides */
        for (r = 1; r < maxr; ++r) {
                sb_move(win, r, 0);
                sb_wca(win, boxp->lbar, attr, 1);
                sb_move(win, r, maxc);
                sb_wca(win, boxp->rbar, attr, 1);
        }

        /* draw bottom row */
        sb_move(win, maxr, 0);
        sb_wca(win, boxp->ll, attr, 1);
        sb_move(win, maxr, 1);
        sb_wca(win, boxp->bbar, attr, x);
        sb_move(win, maxr, maxc);
        sb_wca(win, boxp->lr, attr, 1);

        return SB_OK;
}



makefile for SBUF.LIB
───────────────────────────────────────────────────────────────────────────

# makefile for SBUF.LIB (screen-buffer library)

LLIB = c:\lib\local
LINC = c:\include\local

OBJS = sb_box.obj sb_fill.obj sb_init.obj sb_move.obj sb_new.obj \
sb_put.obj sb_read.obj sb_scrl.obj sb_show.obj sb_write.obj

MODS = sb_box sb_fill sb_init sb_move sb_new.obj sb_put sb_read \
sb_scrl sb_show sb_write

$(LINC)\sbuf.h: sbuf.h
        copy sbuf.h $(LINC)\sbuf.h

$(LINC)\box.h:  box.h
        copy box.h $(LINC)\box.h

sb_box.obj:     sb_box.c $(LINC)\sbuf.h $(LINC)\video.h $(LINC)\box.h

sb_fill.obj:    sb_fill.c $(LINC)\sbuf.h

sb_init.obj:    sb_init.c $(LINC)\sbuf.h

sb_move.obj:    sb_move.c $(LINC)\sbuf.h

sb_new.obj:     sb_new.c $(LINC)\sbuf.h

sb_put.obj:     sb_put.c $(LINC)\sbuf.h

sb_read.obj:    sb_read.c $(LINC)\sbuf.h

sb_scrl.obj:    sb_scrl.c $(LINC)\sbuf.h

sb_show.obj:    sb_show.c $(LINC)\sbuf.h

sb_write.obj:   sb_write.c $(LINC)\sbuf.h

sbuf.lib:       $(OBJS)
        del sbuf.lib
        lib sbuf +$(MODS);
        copy sbuf.lib $(LLIB)



sb_test
───────────────────────────────────────────────────────────────────────────

/*
    *      sb_test -- driver for screen-buffer interface functions
    */

#include <stdio.h>
#include <stdlib.h>
#include <conio.h>
#include <local\std.h>
#include <local\keydefs.h>
#include <local\sbuf.h>
#include <local\video.h>
#include <local\box.h>
#include "sb_test.h"

#define BEL 7

extern struct BUFFER Sbuf;

main(argc, argv)
int argc;
char *argv[];
{
        char *s, line[MAXLINE];
        int k;
        short i;
        FILE *fp;
        char fname[MAXPATH];
        struct REGION *cmnd, *stat, *text, *help, *curwin;
        unsigned char cmndattr, statattr, textattr, helpattr, curattr;
        unsigned char ch, userattr;

        /* function prototypes */
        int sb_init();
        int sb_move(struct REGION *, short, short);
        struct REGION *sb_new(short, short, short, short);
        int sb_putc(struct REGION *, unsigned char);
        int sb_puts(struct REGION *, char *);
        int sb_show(short);
        int sb_fill(struct REGION *, unsigned char, unsigned char);
        char *get_fname(struct REGION *, char *, short);

        getstate();
        readca(&ch, &userattr, Vpage);

        /* set up the screen buffer */
        if (sb_init() == SB_ERR) {
                fprintf(stderr, "Bad UPDATEMODE value in environment\n");
                exit(1);
        }

        /* set up windows and scrolling regions */
        cmnd = sb_new(CMND_ROW, CMND_COL, CMND_HT, CMND_WID);
        stat = sb_new(STAT_ROW, STAT_COL, STAT_HT, STAT_WID);
        text = sb_new(TEXT_ROW, TEXT_COL, TEXT_HT, TEXT_WID);
        help = sb_new(HELP_ROW, HELP_COL, HELP_HT, HELP_WID);
        text->wflags |= SB_SCROLL;
        sb_set_scrl(help, 1, 1, HELP_HT - 1, HELP_WID - 1);

        /* display each primary window in its own attribute */
        cmndattr = GRN;
        statattr = (WHT << 4) | BLK;
        textattr = (BLU << 4) | CYAN;
        helpattr = (GRN << 4) | YEL;
        sb_fill(cmnd, ' ', cmndattr);
        if (sb_move(cmnd, 0, 0) == SB_OK)
                sb_puts(cmnd, "SB_TEST (Version 1.0)");
        sb_fill(stat, ' ', statattr);
        if (sb_move(stat, 0, 0) == SB_OK)
                sb_puts(stat, "*** STATUS AREA ***");
        for (i = 0; i <= text->r1 - text->r0; ++i) {
                sb_move(text, i, 0);
                sb_wca(text, i + 'a', textattr,
                        text->c1 - text->c0 + 1);
        }
        if (sb_move(text, 10, 25) == SB_OK)
                sb_puts(text, " *** TEXT DISPLAY AREA *** ");
        sb_show(Vpage);
        curwin = text;
        curattr = textattr;

        /* respond to user commands */
        while ((k = getkey()) != K_ESC) {
                switch (k) {
                case K_UP:
                        sb_scrl(curwin, 1);
                        break;
                case K_DOWN:
                        sb_scrl(curwin, -1);
                        break;
                case K_PGUP:
                        sb_scrl(curwin, curwin->sr1 - curwin->sr0);
                        break;
                case K_PGDN:
                        sb_scrl(curwin, -(curwin->sr1 - curwin->sr0));
                        break;
                case K_ALTC:
                        /* clear the current window */
                        sb_fill(curwin, ' ', curattr);
                        break;
                case K_ALTH:
                        /* display help */
                        curwin = help;
                        curattr = helpattr;
                        for (i = 0; i < help->r1 - help->r0; ++i) {
                                sb_move(help, i, 0);
                                sb_wca(help, i + 'a', helpattr,
                                        help->c1 - help->c0 + 1);
                        }
                        sb_box(help, BOXBLK, helpattr);
                        break;
                case K_ALTS:
                        /* fill the command area with letters */
                        curwin = stat;
                        curattr = statattr;
                        sb_fill(stat, 's', statattr);
                        break;
                case K_ALTT:
                        /* fill the text area */
                        curwin = text;
                        curattr = textattr;
                        for (i = 0; i <= text->r1 - text->r0; ++i) {
                                sb_move(text, i, 0);
                                sb_wca(text, i + 'a', textattr,
                                        text->c1 - text->c0 + 1);
                        }
                        break;
                case K_ALTR:
                        /* read a file into the current window */
                        sb_fill(stat, ' ', statattr);
                        sb_move(stat, 0, 0);
                        sb_puts(stat, "File to read: ");
                        sb_show(Vpage);
                        (void)get_fname(stat, fname, MAXPATH);
                        if ((fp = fopen(fname, "r")) == NULL) {
                                sb_fill(stat, ' ', statattr);
                                sb_move(stat, 0, 0);
                                sb_puts(stat, "Cannot open ");
                                sb_puts(stat, fname);
                        }
                        else {
                                sb_fill(stat, ' ', statattr);
                                sb_move(stat, 0, 0);
                                sb_puts(stat, "File: ");
                                sb_puts(stat, fname);
                                sb_show(Vpage);
                                sb_fill(text, ' ', textattr);
                                sb_move(text, 0, 0);
                                putcur(text->r0, text->c0, Vpage);
                                while ((s = fgets(line, MAXLINE,
                                        fp)) != NULL) {
                                        if (sb_puts(text, s) == SB_ERR) {
                                                clrscrn(userattr);
                                                putcur(0, 0, Vpage);
                                                fprintf(stderr,
                                                        "puts error\n");
                                                exit(1);
                                        }
                                        sb_show(Vpage);
                                }
                                if (ferror(fp)) {
                                        putcur(text->r0, text->c0, Vpage);
                                        fprintf(stderr,
                                                "Error reading file\n");
                                }
                                fclose(fp);
                        }
                        break;
                default:
                        /* say what? */
                        fputc(BEL, stderr);
                        continue;
                }
                if ((Sbuf.flags & SB_DELTA) == SB_DELTA)
                        sb_show(Vpage);
        }

        clrscrn(userattr);
        putcur(0, 0, Vpage);
        exit(0);
}

/*
    *      get_fname -- get a filename from the user
    */

char *
get_fname(win, path, lim)
struct REGION *win;
char *path;
short lim;
{
        int ch;
        char *s;

        s = path;
        sb_show(Vpage);
        while ((ch = getch()) != K_RETURN) {
                if (ch == '\b')
                        --s;
                else {
                        sb_putc(win, ch);
                        *s++ = ch;
                }
                sb_show(Vpage);
        }
        *s = '\0';
        return (path);
}



ansi.h
───────────────────────────────────────────────────────────────────────────

/*
    *      ansi.h -- header file for ANSI driver information and
    *      macro versions of the ANSI control sequences
    */

/*****************/
/** ANSI macros **/
/*****************/

/* cursor position */
#define ANSI_CUP(r, c)  printf("\x1B[%d;%dH", r, c)
#define ANSI_HVP(r, c)  printf("\x1B[%d;%df", r, c)

/* cursor up, down, forward, back */
#define ANSI_CUU(n)     printf("\x1B[%dA", n)
#define ANSI_CUD(n)     printf("\x1B[%dB", n)
#define ANSI_CUF(n)     printf("\x1B[%dC", n)
#define ANSI_CUB(n)     printf("\x1B[%dD", n)

/* device status report (dumps position data into keyboard buffer) */
#define ANSI_DSR        printf("\x1B[6n")

/* save and restore cursor position */
#define ANSI_SCP        fputs("\x1B[s", stdout)
#define ANSI_RCP        fputs("\x1B[u", stdout)

/* erase display and line */
#define ANSI_ED         fputs("\x1B[2J", stdout);
#define ANSI_EL         fputs("\x1B[K", stdout)

/* set graphic rendition */
#define ANSI_SGR(a)     printf("\x1B[%dm", a)

/* set and reset modes */
#define ANSI_SM(m)      printf("\x1B[=%dh", m)
#define ANSI_RM(m)      printf("\x1B[=%dl", m)


/**********************/
/** ANSI color codes **/
/**********************/

/* special settings */
#define ANSI_NORMAL     0
#define ANSI_BOLD       1
#define ANSI_BLINK      5
#define ANSI_REVERSE    7
#define ANSI_INVISIBLE  8

/* shift values */
#define ANSI_FOREGROUND 30
#define ANSI_BACKGROUND 40

/* basic colors */
#define ANSI_BLACK      0
#define ANSI_RED        1
#define ANSI_GREEN      2
#define ANSI_BROWN      3
#define ANSI_BLUE       4
#define ANSI_MAGENTA    5
#define ANSI_CYAN       6
#define ANSI_WHITE      7


/*****************************/
/** modes for set and reset **/
/*****************************/

#define ANSI_M40        0
#define ANSI_C40        1
#define ANSI_M80        2
#define ANSI_C80        3
#define ANSI_C320       4
#define ANSI_M320       5
#define ANSI_M640       6
#define ANSI_WRAP       7

/* attribute "position" type */
typedef enum {
        FGND, BKGND, BDR
} POSITION;



cpr
───────────────────────────────────────────────────────────────────────────

/*
    *      cpr -- report where the cursor is located
    *      The position information is placed in the keyboard buffer in the
    *      form ESC[rr;ccR, where ESC is the value of the ESCAPE character
    *      and r and c represent decimal values of row and column data.
    */

#include <local\ansi.h>

void ansi_cpr(row, col)
int     *row,
        *col;
{
        int i;

        /* request a cursor position report */
        ANSI_DSR;

        /* toss the ESC and '[' */
        (void) getkey();
        (void) getkey();

        /* read the row number */
        *row = 10 * (getkey() - '0');
        *row = *row + getkey() - '0';

        /* toss the ';' separator */
        (void) getkey();

        /* read the column number */
        *col = 10 * (getkey() - '0');
        *col = *col + getkey() - '0';

        /* toss the trailing ('R') and return */
        (void) getkey();
        (void) getkey();
        return;
}



ansi_tst
───────────────────────────────────────────────────────────────────────────

/*
    *      ansi_tst -- verify that the ANSI.SYS driver is loaded
    *      (prints message and exits if ANSI driver not working)
    */

#include <stdio.h>
#include <local\ansi.h>
#include <local\video.h>

#define TST_ROW 2
#define TST_COL 75

void
ansi_tst()
{
        int row, col;
        static char *help[] = {
                "\n",
                "ANSI.SYS device driver not loaded:\n",
                "  1. Copy ANSI.SYS to your system disk.\n",
                "  2. Add the line device=ansi.sys to your\n",
                "     CONFIG.SYS file and reboot your machine.\n",
                NULL
        };
        char **msg;

        extern int getstate();
        extern int readcur(int *, int *, int);

        getstate();
        ANSI_CUP(TST_ROW, TST_COL);
        readcur(&row, &col, Vpage);
        if (row != TST_ROW - 1 || col != TST_COL - 1) {
                for (msg = help; *msg != NULL; ++msg)
                        fputs(*msg, stderr);
                exit(1);
        }

        return;
}



sc
───────────────────────────────────────────────────────────────────────────

/*
    *  SetColor (sc) -- set foreground, background, and border attributes
    *              on systems equipped with color display systems
    *
    *  Usage:      sc [foreground [background [border]]]
    *              sc [attribute]
    */

#include <stdio.h>
#include <dos.h>
#include <local\std.h>
#include <local\ansi.h>
#include <local\ibmcolor.h>

main(argc, argv)
int argc;
char *argv[];
{
        void ansi_tst();
        BOOLEAN iscolor();
        extern void setattr();
        extern void menumode();

        ansi_tst();
        if (iscolor() == FALSE) {
                fprintf(stderr, "\n\nSystem not in a color text mode.\n");
                fprintf(stderr, "Use the MODE command to set the mode.\n");
                exit(2);
        }

        /* process either batch or interactive commands */
        if (argc > 1)
                /* batch mode processing */
                parse(argc, argv);
        else
                /* no command-line args -- interactive mode */
                menumode();

        ANSI_ED;
        exit (0);
} /* end main() */

/*
    *      iscolor -- return TRUE if a color display system is
    *      in use and is set to one of the text modes
    */

#include <local\std.h>
#include <local\video.h>

BOOLEAN
iscolor()
{
        getstate();
        if (Vmode != CGA_C40 || Vmode != CGA_C80)
                return TRUE;
        return FALSE;
}



parse
───────────────────────────────────────────────────────────────────────────

/*
    *      parse -- process a list of attribute specifications
    */

#include <stdio.h>
#include <local\ansi.h>
#include <local\std.h>
#include <local\ibmcolor.h>

/* buffer length for string comparisons */
#define NCHARS  3
#define C_MASK  0x7

void parse(nargs, argvec)
int nargs;      /* number of argument vectors */
char *argvec[]; /* pointer to the argument vector array */
{
        int i, intensity;
        int attribute;
        POSITION pos;
        char str[NCHARS + 1];
        extern int colornum();
        extern void setattr();

        /* clear all attributes */
        ANSI_SGR(ANSI_NORMAL);

        /* look for a single attribute specification */
        if (nargs == 2) {
                attribute = colornum(argvec[1]);
                switch (attribute) {
                case IBM_NORMAL:
                        palette(0, IBM_BLACK);
                        return;
                case IBM_REVERSE:
                        ANSI_SGR(ANSI_REVERSE);
                        palette(0, IBM_WHITE);
                        return;
                case IBM_INVISIBLE:
                        ANSI_SGR(ANSI_INVISIBLE);
                        return;
                case IBM_BLINK:
                        ANSI_SGR(ANSI_BLINK);
                        return;
                }
        }

        /* must be separate attribute specifications */
        pos = FGND;
        intensity = 0;
        for (i = 1; i < nargs; ++i) {
                attribute = colornum(argvec[i]);
                if (attribute == -1) {
                        ANSI_ED;
                        fprintf(stderr, "\nIllegal parameter\n");
                        exit (2);
                }
                if (attribute == IBM_BRIGHT) {
                        intensity = IBM_BRIGHT;
                        continue;
                }
                setattr(pos, attribute | intensity);
                if (pos == FGND)
                        pos = BKGND;
                else if (pos == BKGND)
                        pos = BDR;
                intensity = 0;
        }
        return;
}



ibmcolor.h
───────────────────────────────────────────────────────────────────────────

/*
    *      ibmcolor.h
    */

/* basic IBM color codes */
#define IBM_BLACK       0
#define IBM_BLUE        1
#define IBM_GREEN       2
#define IBM_CYAN        3
#define IBM_RED         4
#define IBM_MAGENTA     5
#define IBM_BROWN       6
#define IBM_WHITE       7

/* color modifiers */
#define IBM_BRIGHT      8
#define IBM_BLINK       16

/* special attribute codes */
#define IBM_NORMAL      7
#define IBM_REVERSE     112
#define IBM_INVISIBLE   128



colornum
───────────────────────────────────────────────────────────────────────────

/*
    *      colornum -- return the IBM number for the color
    *      presented as a string; return -1 if no match.
    */

#include <stdio.h>
#include <string.h>
#include <local\ibmcolor.h>

#define NCHARS  3

int colornum(name)
char *name;
{
        int n;
        static struct color_st {
                char *c_name;
                int c_num;
        } colortab[] = {
                "black",        IBM_BLACK,
                "blue",         IBM_BLUE,
                "green",        IBM_GREEN,
                "cyan",         IBM_CYAN,
                "red",          IBM_RED,
                "magenta",      IBM_MAGENTA,
                "brown",        IBM_BROWN,
                "white",        IBM_WHITE,
                "normal",       IBM_NORMAL,
                "bright",       IBM_BRIGHT,
                "light",        IBM_BRIGHT,
                "bold",         IBM_BRIGHT,
                "yellow",       IBM_BROWN + IBM_BRIGHT,
                "blink",        IBM_BLINK,
                "reverse",      IBM_REVERSE,
                "invisible",    IBM_INVISIBLE,
                NULL,           (-1)
        };

        (void) strlwr(name);
        for (n = 0; colortab[n].c_name != NULL; ++n)
                if ((strncmp(name, colortab[n].c_name, NCHARS)) == 0)
                        return (colortab[n].c_num);
        return (-1);
}



setattr
───────────────────────────────────────────────────────────────────────────

/*
    *      setattr -- execute an attribute update
    */

#include <stdio.h>
#include <local\ansi.h>
#include <local\ibmcolor.h>

#define C_MASK  0x7

void setattr(pos, attr)
POSITION pos;   /* attribute position */
int attr;       /* composite attribute number (base attr | intensity) */
{
        static int ibm2ansi[] = {
                ANSI_BLACK, ANSI_BLUE, ANSI_GREEN, ANSI_CYAN,
                ANSI_RED, ANSI_MAGENTA, ANSI_BROWN, ANSI_WHITE
        };

        switch (pos) {
        case FGND:
                if (attr & IBM_BRIGHT)
                        ANSI_SGR(ANSI_BOLD);
                ANSI_SGR(ibm2ansi[attr & C_MASK] + ANSI_FOREGROUND);
                break;
        case BKGND:
                if (attr & IBM_BRIGHT)
                        ANSI_SGR(ANSI_BLINK);
                ANSI_SGR(ibm2ansi[attr & C_MASK] + ANSI_BACKGROUND);
                break;
        case BDR:
                palette(0, attr);
                break;
        }
        return;
}



menumode
───────────────────────────────────────────────────────────────────────────

/*
    *  menumode -- process user commands interactively
    */

#include <stdio.h>
#include <local\ansi.h>
#include <local\ibmcolor.h>
#include <local\keydefs.h>

/* maximum color number */
#define MAX_CNUM        15

void menumode()
{
        int ch;
        int foreground, background, border;
        extern void setattr();
        extern void sc_cmds();

        /* default attributes */
        foreground = IBM_WHITE;
        background = IBM_BLACK;
        border = IBM_BLACK;

        ANSI_SGR(ANSI_NORMAL);
        setattr(FGND, foreground);
        setattr(BKGND, background);
        ANSI_ED;
        palette(0, border);
        sc_cmds(foreground, background, border);
        while ((ch = getkey()) != K_RETURN) {
                switch (ch) {
                case K_F1:
                        /* decrement foreground color */
                        if (--foreground < 0)
                                foreground = MAX_CNUM;
                        break;
                case K_F2:
                        /* increment foreground color */
                        if (++foreground > MAX_CNUM)
                                foreground = 0;
                        break;
                case K_F3:
                        /* decrement background color */
                        if (--background < 0)
                                background = MAX_CNUM;
                        break;
                case K_F4:
                        /* increment background color */
                        if (++background > MAX_CNUM)
                                background = 0;
                        break;
                case K_F5:
                        /* decrement border color */
                        if (--border < 0)
                                border = MAX_CNUM;
                        break;
                case K_F6:
                        /* increment border color number */
                        if (++border > MAX_CNUM)
                                border = 0;
                        break;
                default:
                        continue;
                }
                ANSI_SGR(ANSI_NORMAL);
                setattr(FGND, foreground);
                setattr(BKGND, background);
                palette(0, border);
                ANSI_ED;
                sc_cmds(foreground, background, border);
        }
        return;
}



sc_cmds
───────────────────────────────────────────────────────────────────────────

/*
    *      sc_cmds -- display command summary
    */

#include <stdio.h>
#include <local\ansi.h>

void sc_cmds(fg, bkg, bdr)
int fg, bkg, bdr;
{
        static char *color_xlat[] = {
                "Black (0)", "Blue (1)", "Green (2)", "Cyan (3)",
                "Red (4)", "Magenta (5)", "Brown (6)", "White (7)",
                "Grey (8)", "Light blue (9)", "Light green (10)",
                "Light cyan (11)", "Light red (12)", "Light magenta (13)",
                "Yellow (14)", "Bright white (15)"
        };

        ANSI_CUP(2, 29);
        fputs("*** SetColor (SC) ***", stdout);
        ANSI_CUP(4, 17);
        fputs("Attribute  Decrement  Increment  Current Value", stdout);
        ANSI_CUP(5, 17);
        fputs("---------  ---------  ---------  -------------", stdout);
        ANSI_CUP(6, 17);
        fputs("Foreground    F1         F2", stdout);
        ANSI_CUP(7, 17);
        fputs("Background    F3         F4", stdout);
        ANSI_CUP(8, 17);
        fputs("Border        F5         F6", stdout);
        ANSI_CUP(6, 50);
        fputs(color_xlat[fg], stdout);
        ANSI_CUP(7, 50);
        fputs(color_xlat[bkg], stdout);
        ANSI_CUP(8, 50);
        fputs(color_xlat[bdr], stdout);
        ANSI_CUP(10, 17);
        fputs("Type RETURN to exit. SetColor/Version 2.2", stdout);
        return;
} /* end sc_cmds() */



makefile for the SC program
───────────────────────────────────────────────────────────────────────────

# makefile for the SC program

LINC=c:\include\local
LLIB=c:\lib\local

iscolor.obj:    iscolor.c
        msc $*;
        lib $(LLIB)\util -+$*;

colornum.obj:   colornum.c $(LINC)\ansi.h $(LINC)\ibmcolor.h
        msc $*;
        lib color -+$*;

sc_cmds.obj:    sc_cmds.c $(LINC)\ansi.h
        msc $*;
        lib color -+$*;

menumode.obj:   menumode.c $(LINC)\ansi.h $(LINC)\ibmcolor.h
                $(LINC)\keydefs.h
        msc $*;
        lib color -+$*;

parse.obj:      parse.c $(LINC)\ansi.h $(LINC)\ibmcolor.h
        msc $*;
        lib color -+$*;

setattr.obj:    setattr.c $(LINC)\ansi.h $(LINC)\ibmcolor.h
        msc $*;
        lib color -+$*;

sc.obj: sc.c $(LINC)\ansi.h $(LINC)\ibmcolor.h $(LINC)\keydefs.h
        msc $*;

sc.exe: sc.obj color.lib $(LLIB)\ansi.lib $(LLIB)\util.lib
        link $*, $*, nul, color $(LLIB)\ansi $(LLIB)\util $(LLIB)\bios
                $(LLIB)\dos;



useattr
───────────────────────────────────────────────────────────────────────────

/*
    *      userattr -- set video attributes to user-specified values
    *      (DOS environment parameters) or to reasonable defaults and
    *      return success or a failure indication for bad attributes
    */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <local\std.h>
#include <local\ansi.h>
#include <local\ibmcolor.h>

int userattr(foreground, background, border)
char *foreground, *background, *border;
{
        char *s;
        static int attrset();

        if ((s = getenv("FGND")) == NULL)
                s = foreground;
        if (attrset(FGND, s) == -1)
                return FAILURE;

        if ((s = getenv("BKGND")) == NULL)
                s = background;
        if (attrset(BKGND, s) == -1)
                return FAILURE;

        if ((s = getenv("BORDER")) == NULL)
                s = border;
        if (attrset(BDR, s) == -1)
                return FAILURE;

        return SUCCESS;
}

/*
    *      attrset -- parse the color spec and try to set it.
    *      return 0 if OK and -1 upon error (bad color number)
    */
static int attrset(apos, str)
POSITION apos;
char *str;
{
        int attr;
        extern int colornum();
        extern void setattr();

        if ((attr = colornum(strtok(str, " \t"))) == IBM_BRIGHT)
                attr |= colornum(strtok(NULL, " \t"));
        if (attr >= 0)
                setattr(apos, attr);
        else
                return (-1);
        return (0);
}



makefile for the VF program
───────────────────────────────────────────────────────────────────────────

# makefile for the ViewFile (VF) program
# (compile using "compact" memory model)

LIB = c:\lib
LLIB = c:\lib\local
OBJS = vf_list.obj vf_dspy.obj vf_srch.obj vf_util.obj vf_cmd.obj \
        message.obj getstr.obj

vf_list.obj:    vf_list.c vf.h

vf_dspy.obj:    vf_dspy.c vf.h

vf_srch.obj:    vf_srch.c vf.h

vf_util.obj:    vf_util.c vf.h

vf_cmd.obj:     vf_cmd.c vf.h

getstr.obj:     getstr.c

message.obj:    message.c message.h

cvf.lib:                $(OBJS)
        del cvf.lib
        lib cvf +$(OBJS);

vf.obj: vf.c

vf.exe: vf.obj cvf.lib $(LLIB)\cutil.lib $(LLIB)\cdos.lib $(LLIB)\cbios.lib
        link $* $(LIB)\csetargv, $*, NUL, cvf $(LLIB)\cutil $(LLIB)\cdos \
        $(LLIB)\cbios;



vf
───────────────────────────────────────────────────────────────────────────

/*
    *      vf -- view a file using a full-screen window onto
    *      an in-memory text file buffer
    */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <local\std.h>
#include <local\video.h>
#include "vf.h"

extern int setctype(int, int);
int Startscan, Endscan; /* cursor scan line range */
unsigned char Attr;     /* primary display attribute */
unsigned char Revattr;  /* reverse video for highlighting */
unsigned char Usrattr;  /* user's original attribute */

main(argc, argv)
int argc;
char **argv;
{
        int ch;
        static char pgm[MAXNAME + 1] = { "vf" };
        BOOLEAN errflag;
        BOOLEAN numbers;
        int errcode;
        FILE *fp;
        extern char *optarg;
        extern int optind;

        void clean();

        /* external function prototypes */
        extern void getpname(char *, char *);
        extern void fixtabs(int);
        extern void initmsg(int, int, int, unsigned char, int);
        extern int getopt(int, char **, char *);
        extern int vf_cmd(FILE *, char *, BOOLEAN);
        extern int getctype(int *, int *, int);

        errcode = 0;
        getstate();
        fixtabs(TABWIDTH);

        /* get program name from DOS (version 3.00 and later) */
        if (_osmajor >= 3)
                getpname(*argv, pgm);

        /* be sure we have needed DOS support */
        if (_osmajor < 2) {
                fprintf(stderr, "%s requires DOS 2.00 or later\n", pgm);
                exit(1);
        }

        /* process optional arguments first */
        errflag = numbers = FALSE;
        while ((ch = getopt(argc, argv, "n")) != EOF)
                switch (ch) {
                case 'n':
                        /* turn on line numbering */
                        numbers = TRUE;
                        break;
                case '?':
                        /* bad option */
                        errflag = TRUE;
                        break;
                }

        /* check for command-line errors */
        argc -= optind;
        argv += optind;
        if (errflag == TRUE || argc == 0) {
                fprintf(stderr, "Usage: %s [-n] file...\n", pgm);
                exit(1);
        }

        /* get current video attribute and set VF attributes */
        getstate();
        readca(&ch, &Usrattr, Vpage);  /* save user's attribute settings */
        Attr = (BLU << 4) | CYAN;      /* basic text attributes */
        Revattr = (CYAN << 4) | BLK;   /* reverse video for highlighting */
        clrscrn(Attr);

        /* save user's cursor shape */
        getctype(&Startscan, &Endscan, Vpage);
        setctype(MAXSCAN, MAXSCAN);

        /* set up the message line manager */
        initmsg(MSGROW, MSGCOL, Maxcol[Vmode] - MSGCOL, Attr, Vpage);

        /* display first screen page */
        putcur(0, 0, Vpage);
        putstr("ViewFile/1.0  H=Help Q=Quit Esc=Next", Vpage);
        putcur(1, 0, Vpage);
        writea(Revattr, Maxcol[Vmode], Vpage);

        for (; argc-- > 0; ++argv) {
                if ((fp = fopen(*argv, "r")) == NULL) {
                        fprintf(stderr, "%s: cannot open %s -- ", pgm, *argv);
                        perror("");
                        ++errcode;
                        continue;
                }
                if (vf_cmd(fp, *argv, numbers) != 0)
                        break;
        }
        clean();
        exit(errcode);
}


/*
    *      clean -- restore the user's original conditions
    */

void
clean()
{
        /* set screen to user's attribute */
        clrscrn(Attr);
        putcur(0, 0, Vpage);

        /* restore user's cursor shape */
        setctype(Startscan, Endscan);
}



vf.h
───────────────────────────────────────────────────────────────────────────

/*
    *      vf.h -- header for ViewFile program
    */

#define OVERLAP         2
#define MAXSTR          40
#define MSGROW          0
#define MSGCOL          40
#define HEADROW         1
#define TOPROW          2
#define NROWS           23
#define SHIFTWIDTH      20
#define N_NODES         256
#define TABWIDTH        8
#define MAXSCAN         14

typedef enum {
        FORWARD, BACKWARD
} DIRECTION;

/* doubly-linked list structure */
typedef struct dnode_st {
        struct dnode_st *d_next;
        struct dnode_st *d_prev;
        unsigned int d_lnum;    /* file-relative line number */
        unsigned short d_flags; /* miscellaneous line flags */
        char *d_line;           /* pointer to text buffer */
} DNODE;

/* line flags */
#define STANDOUT        0x1



vf_cmd
───────────────────────────────────────────────────────────────────────────

/*
    *      vf_cmd -- ViewFile command processor
    */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <local\std.h>
#include <local\keydefs.h>
#include <local\video.h>
#include "vf.h"
#include "message.h"

extern unsigned char Attr;

int
vf_cmd(fp, fname, numbers)
FILE *fp;
char *fname;
BOOLEAN numbers;
{
        register int i;         /* general index */
        unsigned int offset;    /* horizontal scroll offset */
        unsigned int n;         /* relative line number */
        int memerr;             /* flag for memory allocation errors */
        char *s, lbuf[MAXLINE]; /* input line buffer and pointer */
        int k;                  /* key code (see keydefs.h) */
        int radix = 10;         /* base for number-to-character
                                    *  conversions */
        char number[17];        /* buffer for conversions */
        int errcount = 0;       /* error counter */
        DNODE *tmp;             /* pointer to buffer control nodes */
        DIRECTION srchdir;      /* search direction */
        char *ss;               /* pointer to search string */
        static char srchstr[MAXSTR] = { "" };   /* search string buffer */
        DNODE *head;            /* pointer to starting node of
                                    *  text buffer list */
        DNODE *current;         /* pointer to the current
                                    * node (text line) */
        static DNODE *freelist; /* pointer to starting node of
                                    *  "free" list */
                                /* initialized to 0 at runtime; retains
                                    *   value */

        /* function prototypes */
        static void prtshift(int, int);
        extern DNODE *vf_mklst();
        extern DNODE *vf_alloc(int);
        extern DNODE *vf_ins(DNODE *, DNODE *);
        extern DNODE *vf_del(DNODE *, DNODE *);
        extern DNODE *search(DNODE *, DIRECTION, char *);
        extern DNODE *gotoln(DNODE *);
        extern char *getxline(char *, int, FILE *);
        extern char *getsstr(char *);
        extern int clrscrn(unsigned char);
        extern void showhelp(unsigned char);
        extern void clrmsg();
        extern void vf_dspy(DNODE *, DNODE *, int, BOOLEAN);
        extern int putstr(char *, int);
        extern int writec(char, int, int);
        extern char *nlerase(char *);

        /* display the file name */
        offset = 0;
        putcur(HEADROW, 0, Vpage);
        writec(' ', Maxcol[Vmode], Vpage);
        putstr("File: ", Vpage);
        putstr(fname, Vpage);

        /* establish the text buffer */
        memerr = 0;
        if ((head = vf_mklst()) == NULL)
                ++memerr;
        if (freelist == NULL && (freelist = vf_alloc(N_NODES)) == NULL)
                ++memerr;
        if (memerr) {
                clean();
                fprintf(stderr, "Memory allocation error\n");
                exit(1);
        }

        /* read the file into the buffer */
        current = head;
        n = 0;
        while ((s = getxline(lbuf, MAXLINE, fp)) != NULL) {
                /* add a node to the list */
                if ((freelist = vf_ins(current, freelist)) == NULL)
                        ++memerr;
                current = current->d_next;

                /* save the received text in a line buffer */
                if ((current->d_line = strdup(nlerase(s))) == NULL)
                        ++memerr;
                if (memerr) {
                        clean();
                        fprintf(stderr, "File too big to load\n");
                        exit(1);
                }
                current->d_lnum = ++n;
                current->d_flags = 0;
        }

        /* show the file size as a count of lines */
        putstr(" (", Vpage);
        putstr(itoa(current->d_lnum, number, radix), Vpage);
        putstr(" lines)", Vpage);
        prtshift(offset, Vpage);
        current = head->d_next;
        vf_dspy(head, current, offset, numbers);

        /* process user commands */
        while ((k = getkey()) != K_ESC) {
                clrmsg();
                switch (k) {
                case 'b':
                case 'B':
                case K_HOME:
                        current = head->d_next;
                        break;
                case 'e':
                case 'E':
                case K_END:
                        current = head->d_prev;
                        i = NROWS - 1;
                        while (i-- > 0)
                                if (current->d_prev != head->d_next)
                                        current = current->d_prev;
                        break;
                case K_PGUP:
                case 'u':
                case 'U':
                        i = NROWS - OVERLAP;
                        while (i-- > 0)
                                if (current != head->d_next)
                                        current = current->d_prev;
                        break;
                case K_PGDN:
                case 'd':
                case 'D':
                        i = NROWS - OVERLAP;
                        while (i-- > 0)
                                if (current != head->d_prev)
                                        current = current->d_next;
                        break;
                case K_UP:
                case '-':
                        if (current == head->d_next)
                                continue;
                        current = current->d_prev;
                        break;
                case K_DOWN:
                case '+':
                        if (current == head->d_prev)
                                continue;
                        current = current->d_next;
                        break;
                case K_RIGHT:
                case '>':
                case '.':
                        if (offset < MAXLINE - SHIFTWIDTH)
                                offset += SHIFTWIDTH;
                        prtshift(offset, Vpage);
                        break;
                case K_LEFT:
                case '<':
                case ',':
                        if ((offset -= SHIFTWIDTH) < 0)
                                offset = 0;
                        prtshift(offset, Vpage);
                        break;
                case K_ALTG:
                case 'g':
                case 'G':
                        if ((tmp = gotoln(head)) == NULL)
                                continue;
                        current = tmp;
                        break;
                case K_ALTH:
                case 'h':
                case 'H':
                case '?':
                        showhelp(Attr);
                        break;
                case K_ALTN:
                case 'n':
                case 'N':
                        numbers = (numbers == TRUE) ? FALSE : TRUE;
                        break;
                case K_ALTQ:
                case 'q':
                case 'Q':
                        clrscrn(Attr);
                        putcur(0, 0, Vpage);
                        return (-1);
                case 'r':
                case 'R':
                case '\\':
                        srchdir = BACKWARD;
                        ss = getsstr(srchstr);
                        if (ss == NULL)
                                /* cancel search */
                                break;
                        if (strlen(ss) > 0)
                                strcpy(srchstr, ss);
                        if ((tmp = search(current, srchdir,
                                srchstr)) == NULL)
                                continue;
                        current = tmp;
                        break;
                case 's':
                case 'S':
                case '/':
                        srchdir = FORWARD;
                        ss = getsstr(srchstr);
                        if (ss == NULL)
                                /* cancel search */
                                break;
                        if (strlen(ss) > 0)
                                strcpy(srchstr, ss);
                        if ((tmp = search(current, srchdir,
                                srchstr)) == NULL)
                                continue;
                        current = tmp;
                        break;
                default:
                        /* ignore all other keys */
                        continue;
                }
                vf_dspy(head, current, offset, numbers);
        }
        clrmsg();

        /* release the allocated text buffer memory */
        while (head->d_next != head) {
                /* release text buffer */
                free(head->d_next->d_line);

                /* put node back on the freelist */
                freelist = vf_del(head->d_next, freelist);
        }
        /* release the list header node */
        free((char *)head);

        return (errcount);
}

/*
    *      prtshift -- display the number of columns of horizontal shift
    */

#define SHFTDSP 5

static void
prtshift(amt, pg)
int amt, pg;
{
        char number[17];
        int radix = 10;

        /* clear the shift display area */
        putcur(1, Maxcol[Vmode] - 1 - SHFTDSP, pg);
        writec(' ', SHFTDSP, pg);

        /* display the new shift amount, if any */
        if (amt > 0) {
                putstr(itoa(amt, number, radix), pg);
                putstr("->", pg);
        }
}



putstr
───────────────────────────────────────────────────────────────────────────

/*
    *      putstr -- display a character string in the
    *      prevailing video attribute and return number
    *      characters displayed
    */

int
putstr(s, pg)
register char *s;
int pg;
{
        int r, c, c0;

        readcur(&r, &c, pg);
        for (c0 = c; *s != '\0'; ++s, ++c) {
                putcur(r, c, pg);
                writec(*s, 1, pg);
        }
        putcur(r, c, pg);
        return (c - c0);
}



vf_list
───────────────────────────────────────────────────────────────────────────

/*
    *      vf_list -- linked list management functions
    */

#include <stdio.h>
#include <malloc.h>
#include "vf.h"

/*
    *      vf_mklst -- create a new list by allocating a node,
    *      making it point to itself, and setting its values
    *      to zero (appropriately cast)
    */

DNODE *
vf_mklst()
{
        DNODE *new;

        new = (DNODE *)malloc(sizeof (DNODE));
        if (new != NULL) {
                new->d_next = new;
                new->d_prev = new;
                new->d_lnum = new->d_flags = 0;
                new->d_line = (char *)NULL;
        }
        return (new);
} /* end vf_mklst() */


/*
    *      vf_alloc -- create a pool of available nodes
    */

DNODE *
vf_alloc(n)
int n;
{
        register DNODE *new;
        register DNODE *tmp;

        /* allocate a block of n nodes */
        new = (DNODE *)malloc(n * sizeof (DNODE));

        /* if allocation OK, string the nodes in one direction */
        if (new != NULL) {
                for (tmp = new; 1 + tmp - new < n; tmp = tmp->d_next)
                        tmp->d_next = tmp + 1;
                tmp->d_next = (DNODE *)NULL;
        }
        return (new);   /* pointer to free list */
} /* end vf_alloc() */


/*
    *      vf_ins -- insert a node into a list after the specified node
    */

DNODE *
vf_ins(node, avail)
DNODE *node, *avail;
{
        DNODE *tmp;
        DNODE *vf_alloc(int);

        /*
            *  check freelist -- get another block of nodes
            *  if the list is almost empty
            */
        if (avail->d_next == NULL)
                if ((avail->d_next = vf_alloc(N_NODES)) == NULL)
                        /* not enough memory */
                        return (DNODE *)NULL;

        /* get a node from the freelist */
        tmp = avail;
        avail = avail->d_next;

        /* insert the node into the list after node */
        tmp->d_prev = node;
        tmp->d_next = node->d_next;
        node->d_next->d_prev = tmp;
        node->d_next = tmp;

        /* point to next node in the freelist */
        return (avail);
} /* end vf_ins() */


/*
    *      vf_del -- delete a node from a list
    */

DNODE *
vf_del(node, avail)
DNODE *node, *avail;
{
        /* unlink the node from the list */
        node->d_prev->d_next = node->d_next;
        node->d_next->d_prev = node->d_prev;

        /* return the deleted node to the freelist */
        node->d_next = avail;
        avail = node;

        /* point to the new freelist node */
        return (avail);
} /* end vf_del() */



vf_dspy
───────────────────────────────────────────────────────────────────────────

/*
    *      vf_dspy -- display a screen page
    */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <local\video.h>
#include <local\std.h>
#include <local\bioslib.h>
#include "vf.h"

/* number field width */
#define NFW     8

void
vf_dspy(buf, lp, os, numbers)
DNODE *buf;
register DNODE *lp;
int os;
BOOLEAN numbers;
{
        register int i;
        int j;
        int textwidth;
        char *cp;
        char nbuf[NFW + 1];

        textwidth = Maxcol[Vmode];
        if (numbers == TRUE)
                textwidth -= NFW;

        for (i = 0; i < NROWS; ++i) {
                putcur(TOPROW + i, 0, Vpage);
                cp = lp->d_line;
                if (numbers == TRUE) {
                        sprintf(nbuf, "%6u", lp->d_lnum);
                        putfld(nbuf, NFW, Vpage);
                        putcur(TOPROW + i, NFW, Vpage);
                }
                if (os < strlen(cp))
                        putfld(cp + os, textwidth, Vpage);
                else
                        writec(' ', textwidth, Vpage);
                if (lp == buf->d_prev) {
                        ++i;
                        break;  /* no more displayable lines */
                }
                else
                        lp = lp->d_next;
        }

        /* clear and mark any unused lines */
        for ( ; i < NROWS; ++i) {
                putcur(i + TOPROW, 0, Vpage);
                writec(' ', Maxcol[Vmode], Vpage);
                writec('~', 1, Vpage);
        }
        return;
}



putfld
───────────────────────────────────────────────────────────────────────────

/*
    *      putfld -- display a string in the prevailing
    *      video attribute while compressing runs of a
    *      single character, and pad the field to full width
    *      with spaces if necessary
    */

int
putfld(s, w, pg)
register char *s;       /* string to write */
int w;                  /* field width */
int pg;                 /* screen page for writes */
{
        int r, c, cols;
        register int n;

        extern int putcur(int, int, int);
        extern int readcur(int *, int *, int);
        extern int writec(unsigned char, int, int);

        /* get starting (current) position */
        readcur(&r, &c, pg);

        /* write the string */
        for (n = 0; *s != '\0' && n < w; s += cols, n += cols) {
                putcur(r, c + n, pg);
                /* compress runs to a single call on writec() */
                cols = 1;
                while (*(s + cols) == *s && n + cols < w)
                        ++cols;
                writec(*s, cols, pg);
        }

        /* pad the field, if necessary */
        if (n < w) {
                putcur(r, c + n, pg);
                writec(' ', w - n, pg);
        }

        return (w - n);
}



vf_util
───────────────────────────────────────────────────────────────────────────

/*
    *      vf_util -- utility functions for ViewFile
    */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <local\std.h>
#include <local\video.h>
#include <local\keydefs.h>
#include "vf.h"

extern int Startscan, Endscan;

#define NDIGITS 6

/*
    *      gotoln -- jump to an absolute line number
    */

DNODE *
gotoln(buf)
DNODE *buf;
{
        register int ln;
        register DNODE *lp;
        char line[NDIGITS + 1];

        extern void showmsg(char *);
        extern char *getstr(char *, int);

        /* get line number from user */
        showmsg("Line number: ");
        setctype(Startscan, Endscan);                   /* cursor on */
        ln = atoi(getstr(line, NDIGITS + 1));
        setctype(MAXSCAN, MAXSCAN);                     /* cursor off */

        /* check boundary conditions */
        if (ln > buf->d_prev->d_lnum || ln <= 0) {
                showmsg("Line out of range");
                return ((DNODE *)NULL);
        }

        /* find the line */
        for (lp = buf->d_next; ln != lp->d_lnum; lp = lp->d_next)
                ;
        return (lp);
}


/*
    *      showhelp -- display a help frame
    */

#define HELPROW TOPROW + 3
#define HELPCOL 10
#define VBORDER 1
#define HBORDER 2

void
showhelp(textattr)
unsigned char textattr; /* attribute of text area */
{
        register int i, n;
        int nlines, ncols;
        unsigned char helpattr;
        static char *help[] = {
                "PgUp (U)        Scroll up in the file one screen page",
                "PgDn (D)        Scroll down in the file one screen page",
                "Up arrow (-)    Scroll up in the file one line",
                "Down arrow (+)  Scroll down in the file one line",
                "Right arrow (>) Scroll right by 20 columns",
                "Left arrow (<)  Scroll left by 20 columns",
                "Home (B)        Go to beginning of file buffer",
                "End (E)         Go to end of file buffer",
                "Alt-g (G)       Go to a specified line in the buffer",
                "Alt-h (H or ?)  Display this help frame",
                "Alt-n (N)       Toggle line-numbering feature",
                "\\ (R)          Reverse search for a literal
                                    text string",
                "/ (S)           Search forward for a literal text string",
                "Esc             Next file from list (quits if none)",
                "Alt-q (Q)       Quit",
                "--------------------------------------------------------",
                "            << Press a key to continue >>",
                (char *)NULL
        };

        /* prepare help window */
        ncols = 0;
        for (i = 0; help[i] != (char *)NULL; ++i)
                if ((n = strlen(help[i])) > ncols)
                        ncols = n;
        nlines = i - 1;
        --ncols;
        helpattr = (RED << 4) | BWHT;
        clrw(HELPROW - VBORDER, HELPCOL - HBORDER,
                HELPROW + nlines + VBORDER, HELPCOL + ncols + HBORDER,
                helpattr);
        drawbox(HELPROW - VBORDER, HELPCOL - HBORDER,
                HELPROW + nlines + VBORDER, HELPCOL + ncols + HBORDER,
                Vpage);

        /* display the help text */
        for (i = 0; help[i] != (char *)NULL; ++i) {
                putcur(HELPROW + i, HELPCOL, Vpage);
                putstr(help[i], Vpage);
        }

        /* pause until told by a keypress to proceed */
        getkey();

        /* restore help display area to the text attribute */
        clrw(HELPROW - VBORDER, HELPCOL - HBORDER,
                HELPROW + nlines + VBORDER, HELPCOL + ncols + HBORDER,
                textattr);
}



getstr
───────────────────────────────────────────────────────────────────────────

/*
    *      getstr -- get a string from the keyboard
    */

#include <stdio.h>
#include <string.h>
#include <local\std.h>
#include <local\video.h>
#include <local\keydefs.h>

char *
getstr(buf, width)
char *buf;
int width;
{
        int row, col;
        char *cp;

        /* function prototypes */
        extern int putcur(int, int, int);
        extern int readcur(int *, int *, int);
        extern int writec(char, int, int);
        extern int getkey();

        /* gather keyboard input into a string buffer */
        cp = buf;
        while ((*cp = getkey()) != K_RETURN && cp - buf < width) {
                switch (*cp) {
                case K_CTRLH:
                        /* destructive backspace */
                        if (cp > buf) {
                                readcur(&row, &col, Vpage);
                                putcur(row, col - 1, Vpage);
                                writec(' ', 1, Vpage);
                                --cp;
                        }
                        continue;
                case K_ESC:
                        /* cancel string input operation */
                        return (char *)NULL;
                }
                put_ch(*cp, Vpage);
                ++cp;
        }
        *cp = '\0';
        return (buf);
}



message
───────────────────────────────────────────────────────────────────────────

/*
    *      message -- routines used to display and clear
    *      messages in a reserved message area
    */

#include "message.h"

MESSAGE Ml;
extern int writec(char, int, int);

/*
    *      set up the message-line manager
    */

void
initmsg(r, c, w, a, pg)
int r;                  /* message row */
int c;                  /* message column */
int w;                  /* width of message field */
unsigned char a;        /* message field video attribute */
int pg;                 /* active page for messages */
{
        MESSAGE *mp;
        void clrmsg();

        mp = &Ml;
        mp->m_row = r;
        mp->m_col = c;
        mp->m_wid = w;
        mp->m_attr = a;
        mp->m_pg = pg;
        mp->m_flag = 1;
        clrmsg();
}

/*
    *      showmsg -- display a message and set the message flag
    */

void
showmsg(msg)
char *msg;
{
        MESSAGE *mp;

        mp = &Ml;
        putcur(mp->m_row, mp->m_col, mp->m_pg);
        writec(' ', mp->m_wid, mp->m_pg);
        putstr(msg, mp->m_pg);
        mp->m_flag = 1;
        return;
}

/*
    *      clrmsg -- erase the message area and reset the message flag
    */

void
clrmsg()
{
        MESSAGE *mp;

        mp = &Ml;
        if (mp->m_flag != 0) {
                putcur(mp->m_row, mp->m_col, mp->m_pg);
                writec(' ', mp->m_wid, mp->m_pg);
                mp->m_flag = 0;
        }
        return;
}



vf_srch
───────────────────────────────────────────────────────────────────────────

/*
    *      vf_srch -- search functions
    */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <local\std.h>
#include "vf.h"

/*
    *      search -- search for a literal string in the buffer
    */

DNODE * search(buf, dir, str)
DNODE *buf;
DIRECTION dir;
char *str;
{
        int n;
        register DNODE *lp;
        register char *cp;
        extern void showmsg(char *);

        /* try to find a match -- wraps around buffer boundaries */
        n = strlen(str);
        lp = (dir == FORWARD) ? buf->d_next : buf->d_prev;
        while (lp != buf) {
                if ((cp = lp->d_line) != NULL)  /* skip over header node */
                        while (*cp != '\n' && *cp != '\0') {
                                if (strncmp(cp, str, n) == 0)
                                        return (lp);
                                ++cp;
                        }
                lp = (dir == FORWARD) ? lp->d_next : lp->d_prev;
        }
        showmsg("Not found");
        return ((DNODE *)NULL);
}


/*
    *      getsstr -- prompt the user for a search string
    */

extern int Startscan, Endscan;

char *getsstr(str)
char *str;
{
        char line[MAXSTR];
        char *resp;
        extern int putstr(char *, int);
        extern char *getstr(char *, int);
        extern int put_ch(char, int);
        extern void showmsg(char *);
        extern void clrmsg();
        static char prompt[] = { "Search for: " };

        /* get search string */
        showmsg(prompt);
        setctype(Startscan, Endscan);           /* cursor on */
        resp = getstr(line, MAXSTR - strlen(prompt));
        setctype(MAXSCAN, MAXSCAN);             /* cursor off */
        if (resp == NULL)
                return (char *)NULL;
        if (strlen(resp) == 0)
                return (str);
        showmsg(resp);
        return (resp);
}



getxline
───────────────────────────────────────────────────────────────────────────

/*
    *   getxline -- get a line of text while expanding tabs,
    *   put text into an array, and return a pointer to the
    *   resulting null-terminated line
    */

#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <local\std.h>

char *getxline(buf, size, fin)
char *buf;
int size;
FILE *fin;
{
        register int ch;    /* input character */
        register char *cp;  /* character pointer */
        extern BOOLEAN tabstop(int);

        cp = buf;
        while (--size > 0 && (ch = fgetc(fin)) != EOF) {
                if (ch == '\n') {
                        *cp++ = ch;
                        break;
            }
            else if (ch == '\t')
                    do {
                            *cp = ' ';
                    } while (--size > 0 && (tabstop(++cp - buf) == FALSE));
            else
                    *cp++ = ch & ASCII;
        }
        *cp = '\0';
        return ((ch == EOF && cp == buf) ? NULL : buf);
}




Index


Page numbers for illustrations are in italics


Symbols
──────────────────────────────────────────────────────────────────────
#
%var%
/ (DOS option switch)
\ (DOS pathname separator)
\\ (C escape)
\include
\include\local
\lib
\lib\local
\x
|


A
──────────────────────────────────────────────────────────────────────
Advanced MS-DOS
affirm()
Alt key status
American National Standards Institute (ANSI)
    basics
    color attributes
    control sequences
    device driver (see ANSI.SYS)
    display memory
    local library summary
    proposed standard for C language
ansi_cpr()
ANSI_CUP
ANSI_DSR
ansi.h
ANSI_SGR
ANSI.SYS
    control codes
        numeric parameters
        selective parameters
    installing
    interface package
    pros and cons of
    SetColor program
    set graphic rendition (SGR)
    user-attribute function
ansi_tst()
    SetColor program
arguments
    argc
    argv
    fixed/variable-list
    pathname and type
    video row and col
ASCII codes. See also displaying non-ASCII text
                and ANSI controls
    block characters
    character code data bytes
    control character table
    for Epson MX/FX printer control
    extended
    line-drawing characters
    printable character table
    video attributes
asctime()
Aspen Scientific
Assembler
AT&T
    PC6300
    UNIX System V
atoi()
attributes. See characters and attributes
AUTOEXEC.BAT file
automatic program configuration
    printer control functions
    selection functions
    using configuration files
    using the program name


B
──────────────────────────────────────────────────────────────────────
batch processing
    SetColor program
bdos()
beep()
binary numbers, converting
bios.lib
bioslib.h
BIOS library routines
    demonstration program
    equipment determination functions
    keyboard status
    library summary
    makefile
    video access
bios.mk
blink bit
block characters
block-copy routine
    synchronized
blocking read
box-drawing functions
box.h header file
buffer(s)
    input/output
    screen (see buffered screen interface; screen access routines)
    text (see text buffer)
buffered screen interface
    box-drawing functions
    buffer management functions
    general window functions
    interface package
    screen buffer library
byte2hex()


C
──────────────────────────────────────────────────────────────────────
call by reference
call by value
Caps Lock key status
CAT program
cells, buffer
character I/O functions
    characters and attributes.
    See also ASCII codes; SetColor (SC) program
    display memory
    reading from screen buffers
    writing to windows
char data type
C language
    DOS-to-, (see DOS-to-C connection)
    language details
    Microsoft, version 4.00
    PC interfaces (see PC operating system interfaces)
    standard routines (see standard libraries)
    standards/compatibility
    technical highlights
    user interface (see user interface)
CL control program
clean()
clearerr()
clock. See PC timer
close()
clrmsg()
clrprnt()
clrscrn()
clrw()
CodeView
color graphics adapter (CGA)
    display adapter basics
    memory interference
colornum()
color numbers
colors. See SetColor (SC) program
command-line processing
    accessing the command line
    ambiguous filenames
    arguments
    pointers and indirection
    program name
    standardizing, to improve user interface
compact memory model
compatibility. See portability
compiler
    Microsoft version 4.00
    other compilers for DOS
Computer Innovations C86
configuration. See automatic program configuration
configuration files
confirm()
conversion functions, numbers
cpblk()
    screen updates
    ST program
CP program
The C Programming Language
"C Reference Manual"
CR/LF
ctime()
Ctrl-Break
Ctrl key status
Ctrl-Z
Curses
cursor
    control functions
    position report
    restoring
    split
    window
cursor.c
cursor.mk
CURSOR program
    makefile
    output
    pseudocode
    source code


D
──────────────────────────────────────────────────────────────────────
DEBUG program. See also DUMP program
debugging programs. See also CodeView
decimal numbers, converting
delay()
device driver. See ANSI.SYS
device status report
directories
    disk
    list (see LS utility program)
    pathname (see PWD utility program)
direct screen access
    alternative solutions
    display adapter basics
    programming considerations
disk routine numbers, BIOS
display adapter basics
displaying non-ASCII text
    conversion functions
    DUMP
    SHOW
display memory
display system type, determining
do_rm()
DOS
    compared to UNIX and XENIX
    error messages
    library (see DOS library routines)
    link (see LINK (3.51) program)
    -to-C (see DOS-to-C connection)
    version number
DOS environment
    pointer
    reserved space
    setting values correctly
    variables
dos.h
dos.lib
doslib.h
DOS library routines
    demonstration program
    DOS version number
    keyboard functions
    summary
DOS-to-C connection
    command-line processing
    DOS environment variables
    input/output redirection and piping
double buffering
drawbox()
drvpath()
DSPYTYPE program
dump.c
dump.mk
DUMP program
    makefile
    manual page
    output
    pseudocode
    source code
Duncan, Ray


E
──────────────────────────────────────────────────────────────────────
ECHO command
EDITOR
EDLIN program
ega_info()
element, in display memory
enhanced graphics adapter (EGA)
enum type specifier
environment pointer
environment variables
ENVSIZE program
EOF (end of file) indicator
epoch time
Epson dot-matrix printer
    interface routines
    MX program
equipchk()
equip.h
equipment
    determination functions
    standards
ERASE/DEL command
errno
error functions
    ambiguous return values
    getreply()
    operating system interrupts
ERRORLEVEL
exception handling
exec()
execl()
EXEMOD
EXEPACK
exit()
external storage for large files


F
──────────────────────────────────────────────────────────────────────
fatal()
fclose()
fcloseall()
fconfig()
fcopy()
feof()
ferror()
fgetc()
fgetchar()
fgets()
file(s)
    basic functions
    configuration (see configuration files)
    names (see filenames, ambiguous)
    network sharing
    nontext (see displaying non-ASCII text)
    printing (see file printing)
    utilities (see file utilities)
    viewing (see ViewFile (VF) program)
filenames, ambiguous
    wildcards
file printing
    file handling
    option handling
    possible enhancements to
    program maintenance
    program specification
file utilities
filter
    DUMP as
    PR as
first_fm()
fit()
fixtabs()
floating point
fopen()
FORTRAN
fprintf()
fputc()
fputs()
free()
fseek()
function(s). See also names of individual functions
    classified by environment
    error
    file and character I/O
    keyboard
    library management of
    local library summary
    number conversion
    printer control
    process control
    selection
    suffix meanings
    system interface
    time/date
    timing
    user attribute
    window
function keys


G
──────────────────────────────────────────────────────────────────────
game port
getc()
getch()
getchar()
getctype()
getcwd()
getdrive()
getenv()
getkey()
getname()
getopt()
    pseudocode
    source
getpname()
getreply()
getstate()
    to determine video mode
getstr()
getticks()
getxline()
global variables
gmtime()
gotoln()
graphic rendition
Greenwich mean time (GMT)


H
──────────────────────────────────────────────────────────────────────
hardware cursor
hexadecimal character constants
hexadecimal numbers, converting
hex.c
hexdump()
horizontal sweep signal


I
──────────────────────────────────────────────────────────────────────
ibmcolor.h
initmsg()
input/output (I/O)
    buffered
    character functions
    redirection and piping
        with TEE
    terminating
    user input
Ins key status
intdos()
intdosx()
int86()
int86x()
interactive mode, SETCOLOR program
interlanguage calls
interrupts
    BIOS
    PC operating system
interval()
intra-application communications area (ICA)
iscolor()


K
──────────────────────────────────────────────────────────────────────
kbd_stat()
kbhit()
Kernighan, Brian. See also Ritchie, Dennis M.
keybdlib.h
keyboard
    BIOS routine numbers
    functions (DOS)
    status
    stdin stream
keydefs.h
keyready()


L
──────────────────────────────────────────────────────────────────────
language(s)
    details, Microsoft C
    interlanguage calls
last_ch()
Lattice-MS-DOS C Compiler
LF (linefeed) character
LIB, object-file maintainer
libraries. See local library summary; screen buffer library;
        standard libraries
linebuf.h
line-drawing characters
lines()
LINK (3.51) program
list directories. See LS utility program
local library summary
    ANSI
    BIOS
    DOS
    screen buffer
    utility
localtime()
ls.c
ls_dirx()
lseek()
ls_fcomp()
ls_list()
ls.mk
ls_multi()
ls_single()
LS utility program
    makefile
    manual page
    pseudocode
    source code


M
──────────────────────────────────────────────────────────────────────
macros. See function(s)
"magic cookies" attribute positions
mainmenu()
MAKE command program maintainer. See also makefile
makefile
    ANSI
    BIOS
    CURSOR
    DUMP
    LS
    MX
    PR
    PRTSTR
    REPLAY
    sbuf.lib
    SetColor
    SHOW
    SHOWTABS
    ST
    ViewFile
malloc()
Mark Williams C Programming System (MWC)
MASM macro assembler
math coprocessor
math libraries, multiple
memchk()
memcpy()
memory. See also buffer(s)
    checking, to determine screen type
    compact model
    determining total
    display (see display memory)
    intra-application communications area (ICA)
    models
memsize()
message.c
message.h
Microsoft C, version 4.00
    language details
    technical highlights
Microsoft C Compiler Library Reference Manual
mkslist()
    pseudocode
MODE command
modeflag
monochrome adapter
MORE
movedata()
MSC control program
MULTICOLUMN variable
mx.mk
MX program
    makefile
    manual page
    source code


N
──────────────────────────────────────────────────────────────────────
next_fm()
NL (newline) character
nlerase()
non-ASCII text. See displaying non-ASCII text
nonblocking read
NOTES program
NOTES2 program
_nullcheck()
numbers, conversion functions
Num Lock key status


O
──────────────────────────────────────────────────────────────────────
open()
operating system
    interrupts
    PC interfaces (see PC operating system interfaces)
optarg
opterr
Optimizing C86
optind
option flag(s)
option switches
_osmajor
_osminor
OUTBUF
overlays


P
──────────────────────────────────────────────────────────────────────
page(s)
    active/visual
    "flipping"
    formatting/copying text
    layout
Pagelist
palette()
parse()
Pascal
pathname
    display
    and type argument
PC operating system interfaces
    BIOS library routines
    demonstration program (CURSOR)
    DOS library routines
    functions
    library management issues
PC timer
p_dest
perror()
pointer(s)
    environment
    and indirection
PolyMake
portability
P_OVERLAY
pr.c
pr_cpy()
    pseudocode
    source code
preprocessor directives
pr_file()
pr_gcnf()
    pseudocode
    source code
pr_getln()
pr_help()
PRINT
printer control functions
    MX program
    PRTSTR program
printer.c
printer.h
printf()
print.h
printing files. See file printing
print working directory path. See PWD utility program
prlib.lib
pr_line()
pr.mk
pr.obj
process control functions
program development
    development cycle
    guiding principles
    local environment
programmable peripheral interface (PPI)
programming languages
    interlanguage calls
program name
    using, to alter program behavior
program size
PR program
    calling hierarchy
    file handling
    as filter
    folded lines
    formatting/copying text pages
    makefile
    manual page
    option handling
    page layout
    possible enhancements
    program maintenance
    as a sink
    source code
    specification
prtshift()
prtstr.c
prtstr.mk
PRTSTR program
    makefile
    manual page
    source code
pseudocode
    for configuration layers
    CURSOR
    DUMP
    getopt()
    mkslist()
    pr_cpy()
    pr_gcnf()
    RM
    save_range
    showit()
    TEE
    TOUCH
putc()
put_ch()
putcur()
putenv()
putfld()
putstr()
pwd.c
PWD utility program
    manual page
    source code


Q
──────────────────────────────────────────────────────────────────────
qsort()


R
──────────────────────────────────────────────────────────────────────
read
    blocking
    characters/attributes from screen buffers
    from the DOS environment
    nonblocking
    video characters
read()
readca()
readcur()
readdot()
realloc()
REGION
registers
    structures
    word/byte
remove files. See RM utility program
replay.c
replay.mk
REPLAY program
    makefile
return values, ambiguous
rewind()
Ritchie, Dennis M.. See also Kernighan, Brian
rm.c
RM utility program
    manual page
    pseudocode
    source code
routines. See function(s)
rows and columns, video
RUN_ONCE program


S
──────────────────────────────────────────────────────────────────────
SAMPLE.BAT file
save_range()
    pseudocode
sb_box()
sb_fill()
sb_init()
sb_move()
sb_new()
sb_putc()
sb_puts()
sb_ra()
sb_rc()
sb_rca()
sb_read()
sb_scrl()
sb_set_scrl()
sb_show()
sb_test.c
SB_TEST driver program
    header file
    makefile
    source code
sb_test.mk
sbuf.h
sbuf.lib
    makefile
sb_wa()
sb_wc()
sb_wca()
sb_write()
scanf()
sc.c
sc_cmds()
Schinnell, Rich
sc.mk
screen(s)
    access (see buffered screen interface;
        screen access routines; video access)
    color/graphics adapter basics
    determining type of
    scrolling/clearing commands
    stderr/stdout streams
    updates
    updates with cpblk()
    user input
screen access routines. See also buffered screen interface
    demonstration program ST
    design considerations
    determining display system type
    direct screen access
    double buffering
    synchronized block-copy routine
screen buffer library
scroll()
scrolling windows/screens
    commands
Scroll Lock key status
search()
segread()
select.c
selected()
selection functions
SELECT module routines
setargv()
setattr()
SetColor (SC) program
    batch mode
    function keys in
    interactive mode
        makefile
    manual page
    pseudocode
    source code
    using
SET command
setctype()
setdta()
_setenvp()
SETENV program
setfont()
setfreq()
set graphic rendition (SGR)
SETMYDIR program
setpage()
setprnt()
setvmode()
sflag
shift key status
show.c
SHOWENV program
showhelp()
showit()
show.mk
showmsg()
SHOW program
    makefile
    manual page
    pseudocode
    source code
SHOWTABS program
    makefile
    output
signal()
signal catching
sink. See also filter
Slist
sorting
sound()
sound.c
sound generation
sound.h
SOUNDS program
spaces()
spawn()
spawnvp()
SPKR program
standard data streams
standard libraries. See also function(s)
    basic file and character I/O functions
    BIOS routines
    DOS routines
    exception handling
    functions management
    LIB
    other library functions
    process control functions
    reasons for using
    system interface functions
    time functions
st.c
stderr (standard error) stream
std.h
stdin (standard input) stream
stdio.h
stdlib.h
stdout (standard output) stream
st.mk
ST (screen test) program
    makefile
    source code
strdup()
string(s)
    creating time
    into windows
strlwr()
strtok()
struct BYTEREGS
struct pr_st
struct tm
structure handling
struct WORDREGS
strupr()
support tools for C compiler
swap_int()
sweep.c
switches. See option switches
SYMDEB program
system()


T
──────────────────────────────────────────────────────────────────────
tabs.c
tabstop()
tab stops, setting
tee.c
TEE utility program
    manual page
    pseudocode
    source code
Termcap
testing programs
text buffer
    management
TICKRATE
time()
time conversions
    creating strings
    time zones
timedata.c
TIMEDATA program
time/date functions
time delays
time.h
TIMER program
    manual page
timing functions
audible feedback
PC timer and time delays
TONE program
tools.ini
touch.c
TOUCH utility program
    manual page
    pseudocode
    source code
TSTREPLY program
tstsel.c
TSTSEL program
    output
typedef
TZ
tzset()


U
──────────────────────────────────────────────────────────────────────
Universal time (UT)
"UNIX 1984 Standard"
UNIX operating system
    command-line processing
    compared to DOS
    control program (cc)
    ctime()
    getopt() error
    MAKE program
    portability
    standardization of
    starting optional arguments (-)
    System V
unlink()
UPDATEMODE
userattr()
user interface
    command-line processing
    timing functions
    user input
utility programs
    library summary
util.lib


V
──────────────────────────────────────────────────────────────────────
variables
    DOS environment
    global (see global variables)
    time
vartabs()
ver()
vf_alloc()
vf_cmd()
vf_cmd.c
vf_del()
vf_dspy()
vf.h
vf_ins()
vflag
vf_list.c
vf.mk
vf_mklst()
vf_srch.c
vf_util.c
video access. See also screen(s)
video attributes
video.h
video routine numbers, BIOS
ViewFile (VF) program
    external storage
    features
    header file
    help frame
    implementation details
    makefile
    manual page
    source code
void


W
──────────────────────────────────────────────────────────────────────
warn()
wflag
wildcard characters in ambiguous filenames
window(s)
    general functions
    REGION
word2hex()
write
    characters/attributes to windows
    to the DOS environment
    video characters
write()
writea()
writec()
writeca()
writedot()
writemsg()
writestr()
writetty()


X
──────────────────────────────────────────────────────────────────────
XENIX
        compared to DOS
        control program (cc)
        getopt() error
        portability