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 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 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 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 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 #include 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 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 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 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 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 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 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 int execl(pathname, arg0, arg1, ..., argn, NULL); char *pathname; char *arg0, *arg1, . . .,*argn; The declaration for spawnvp() is #include 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 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 /************************************************************ * 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 #include #include #include 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 #include 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 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 #include #include 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 #include #include 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 #include #include 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 #include #include 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 #include #include 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 #include #include 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 #include 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 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 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 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 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 #include 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 #include 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 #include 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 #include 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 #include 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 #include #include 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 ────────────────────── 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 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 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 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 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 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 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 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 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 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 #include 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 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 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 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 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 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 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 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 #include 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 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 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 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 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 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 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 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 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 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 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 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 #include #include 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 #include 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 #include #include #include 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 #include #include 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 #include 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 #include #include #include 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(<ime)); fprintf(fp, "%s\n", ctime(<ime)); /* 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 #include #include #include #include #include /* 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(<ime)); 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 #include #include #include 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 #include #include 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 #include #include /* 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 #include #include /*********************************************************** * 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 #include #include #include 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 #include #include 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 #include #include 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 #include #include #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 #include #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 #include #include 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 #include #include 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 #include #include #include 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 #include #include 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 #include #include 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 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 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 #include #include #include /* 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 #include 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 #include #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 #include #include 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 #include #include #include #include #include #include #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 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 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 #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 #include 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 #include 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 #include #include #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 #include #include #include #include #include #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 #include #include #include #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 #include #include #include #include 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 #include #include #include #include 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 #include #include #include 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 #include #include 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 #include #include #include #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 #include #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 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 #include #include #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 #include #include #include #include #include #include #include #include /* 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 #include #include #include 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 #include #include 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 #include #include #include #include #include #include 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 #include #include #include #include #include #include #include #include #include #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 #include #include #include #include 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 #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 #include #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 #include #include #include #include #include #include #include #include #include #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 #include 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 #include 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 #include 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 #include #include #include #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 #include #include #include #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 #include #include #include #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 #include #include #include #include #include #include #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(<ime), 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 #include #include 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 #include #include #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 #include #include #include #include #include #include #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 #include #include #include #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 #include #include #include #include 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 #include #include 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 #include #include #include #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 #include 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 #include #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 #include #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 #include #include #include #include #include #include #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 #include #include #include /* 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 #include #include #include #include #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 #include #include 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 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 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 #include 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 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 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 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 #include #include 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 #include #include #include #include #include #include #include #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 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 #include #include #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 #include #include #include #include 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 #include BOOLEAN iscolor() { getstate(); if (Vmode != CGA_C40 || Vmode != CGA_C80) return TRUE; return FALSE; } parse ─────────────────────────────────────────────────────────────────────────── /* * parse -- process a list of attribute specifications */ #include #include #include #include /* 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 #include #include #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 #include #include #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 #include #include #include /* 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 #include 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 #include #include #include #include #include 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 #include #include #include #include #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 #include #include #include #include #include #include #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 #include #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 #include #include #include #include #include #include #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 #include #include #include #include #include #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 #include #include #include #include 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 #include #include #include #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 #include #include #include 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