Example Programs: SHELL.C and SHELL.ASM

As a practical example of use of the MS-DOS EXEC function, I have included a small command interpreter called SHELL, with equivalent Microsoft C (Figure 12-4) and Microsoft Macro Assembler (Figure 12-5) source code. The source code for the assembly-language version is considerably more complex than the code for the C version, but the names and functionality of the various procedures are quite parallel.

/*

SHELL.C Simple extendable command interpreter

for MS-DOS versions 2.0 and later

Copyright 1988 Ray Duncan

Compile: C>CL SHELL.C

Usage: C>SHELL

*/

#include <stdio.h>

#include <process.h>

#include <stdlib.h>

#include <signal.h>

/* macro to return number of

elements in a structure */

#define dim(x) (sizeof(x) / sizeof(x[0]))

unsigned intrinsic(char *); /* function prototypes */

void extrinsic(char *);

void get_cmd(char *);

void get_comspec(char *);

void break_handler(void);

void cls_cmd(void);

void dos_cmd(void);

void exit_cmd(void);

struct cmd_table { /* intrinsic commands table */

char *cmd_name;

int (*cmd_fxn)();

} commands[] =

{ "CLS", cls_cmd,

"DOS", dos_cmd,

"EXIT", exit_cmd, };

static char com_spec[64]; /* COMMAND.COM filespec */

main(int argc, char *argv[])

{

char inp_buf[80]; /* keyboard input buffer */

get_comspec(com_spec); /* get COMMAND.COM filespec */

/* register new handler

for Ctrl-C interrupts */

if(signal(SIGINT, break_handler) == (int(*)()) -1)

{

fputs("Can't capture Control-C Interrupt", stderr);

exit(1);

}

while(1) /* main interpreter loop */

{

get_cmd(inp_buf); /* get a command */

if (! intrinsic(inp_buf) ) /* if it's intrinsic,

run its subroutine */

extrinsic(inp_buf); /* else pass to COMMAND.COM */

}

}

/*

Try to match user's command with intrinsic command

table. If a match is found, run the associated routine

and return true; else return false.

*/

unsigned intrinsic(char *input_string)

{

int i, j; /* some scratch variables */

/* scan off leading blanks */

while(*input_string == '\x20') input_string++ ;

/* search command table */

for(i=0; i < dim(commands); i++)

{

j = strcmp(commands[i].cmd_name, input_string);

if(j == 0) /* if match, run routine */

{

(*commands[i].cmd_fxn)();

return(1); /* and return true */

}

}

return(0); /* no match, return false */

}

/*

Process an extrinsic command by passing it

to an EXEC'd copy of COMMAND.COM.

*/

void extrinsic(char *input_string)

{

int status;

status = system(input_string); /* call EXEC function */

if(status) /* if failed, display

error message */

fputs("\nEXEC of COMMAND.COM failed\n", stderr);

}

/*

Issue prompt, get user's command from standard input,

fold it to uppercase.

*/

void get_cmd(char *buffer)

{

printf("\nsh: "); /* display prompt */

gets(buffer); /* get keyboard entry */

strupr(buffer); /* fold to uppercase */

}

/*

Get the full path and file specification for COMMAND.COM

from the COMSPEC variable in the environment.

*/

void get_comspec(char *buffer)

{

strcpy(buffer, getenv("COMSPEC"));

if(buffer[0] == NULL)

{

fputs("\nNo COMSPEC in environment\n", stderr);

exit(1);

}

}

/*

This Ctrl-C handler keeps SHELL from losing control.

It just reissues the prompt and returns.

*/

void break_handler(void)

{

signal(SIGINT, break_handler); /* reset handler */

printf("\nsh: "); /* display prompt */

}

/*

These are the subroutines for the intrinsic commands.

*/

void cls_cmd(void) /* CLS command */

{

printf("\033[2J"); /* ANSI escape sequence */

} /* to clear screen */

void dos_cmd(void) /* DOS command */

{

int status;

/* run COMMAND.COM */

status = spawnlp(P_WAIT, com_spec, com_spec, NULL);

if (status)

fputs("\nEXEC of COMMAND.COM failed\n",stderr);

}

void exit_cmd(void) /* EXIT command */

{

exit(0); /* terminate SHELL */

}

Figure 12-4. SHELL.C: A table-driven command interpreter written in Microsoft C.

name shell

page 55,132

title SHELL.ASM--simple MS-DOS shell

;

; SHELL.ASM Simple extendable command interpreter

; for MS-DOS versions 2.0 and later

;

; Copyright 1988 by Ray Duncan

;

; Build: C>MASM SHELL;

; C>LINK SHELL;

;

; Usage: C>SHELL;

;

stdin equ 0 ; standard input handle

stdout equ 1 ; standard output handle

stderr equ 2 ; standard error handle

cr equ 0dh ; ASCII carriage return

lf equ 0ah ; ASCII linefeed

blank equ 20h ; ASCII blank code

escape equ 01bh ; ASCII escape code

_TEXT segment word public 'CODE'

assume cs:_TEXT,ds:_DATA,ss:STACK

shell proc far ; at entry DS = ES = PSP

mov ax,_DATA ; make our data segment

mov ds,ax ; addressable

mov ax,es:[002ch] ; get environment segment

mov env_seg,ax ; from PSP and save it

; release unneeded memory...

; ES already = PSP segment

mov bx,100h ; BX = paragraphs needed

mov ah,4ah ; function 4ah = resize block

int 21h ; transfer to MS-DOS

jnc shell1 ; jump if resize OK

mov dx,offset msg1 ; resize failed, display

mov cx,msg1_length ; error message and exit

jmp shell4

shell1: call get_comspec ; get COMMAND.COM filespec

jnc shell2 ; jump if it was found

mov dx,offset msg3 ; COMSPEC not found in

mov cx,msg3_length ; environment, display error

jmp shell4 ; message and exit

shell2: mov dx,offset shell3 ; set Ctrl-C vector (int 23h)

mov ax,cs ; for this program's handler

mov ds,ax ; DS:DX = handler address

mov ax,2523h ; function 25h = set vector

int 21h ; transfer to MS-DOS

mov ax,_DATA ; make our data segment

mov ds,ax ; addressable again

mov es,ax

shell3: ; main interpreter loop

call get_cmd ; get a command from user

call intrinsic ; check if intrinsic function

jnc shell3 ; yes, it was processed

call extrinsic ; no, pass it to COMMAND.COM

jmp shell3 ; then get another command

shell4: ; come here if error detected

; DS:DX = message address

; CX = message length

mov bx,stderr ; BX = standard error handle

mov ah,40h ; function 40h = write

int 21h ; transfer to MS-DOS

mov ax,4c01h ; function 4ch = terminate with

; return code = 1

int 21h ; transfer to MS-DOS

shell endp

intrinsic proc near ; decode user entry against

; the table "COMMANDS"

; if match, run the routine,

; and return carry = false

; if no match, carry = true

; return carry = true

mov si,offset commands ; DS:SI = command table

intr1: cmp byte ptr [si],0 ; end of table?

je intr7 ; jump, end of table found

mov di,offset inp_buf ; no, let DI = addr of user input

intr2: cmp byte ptr [di],blank ; scan off any leading blanks

jne intr3

inc di ; found blank, go past it

jmp intr2

intr3: mov al,[si] ; next character from table

or al,al ; end of string?

jz intr4 ; jump, entire string matched

cmp al,[di] ; compare to input character

jnz intr6 ; jump, found mismatch

inc si ; advance string pointers

inc di

jmp intr3

intr4: cmp byte ptr [di],cr ; be sure user's entry

je intr5 ; is the same length...

cmp byte ptr [di],blank ; next character in entry

jne intr6 ; must be blank or return

intr5: call word ptr [si+1] ; run the command routine

clc ; return carry flag = false

ret ; as success flag

intr6: lodsb ; look for end of this

or al,al ; command string (null byte)

jnz intr6 ; not end yet, loop

add si,2 ; skip over routine address

jmp intr1 ; try to match next command

intr7: stc ; command not matched, exit

ret ; with carry = true

intrinsic endp

extrinsic proc near ; process extrinsic command

; by passing it to

; COMMAND.COM with a

; " /C " command tail

mov al,cr ; find length of command

mov cx,cmd_tail_length ; by scanning for carriage

mov di,offset cmd_tail+1 ; return

cld

repnz scasb

mov ax,di ; calculate command-tail

sub ax,offset cmd_tail+2 ; length without carriage

mov cmd_tail,al ; return, and store it

; set command-tail address

mov word ptr par_cmd,offset cmd_tail

call exec ; and run COMMAND.COM

ret

extrinsic endp

get_cmd proc near ; prompt user, get command

; display the shell prompt

mov dx,offset prompt ; DS:DX = message address

mov cx,prompt_length ; CX = message length

mov bx,stdout ; BX = standard output handle

mov ah,40h ; function 40h = write

int 21h ; transfer to MS-DOS

; get entry from user

mov dx,offset inp_buf ; DS:DX = input buffer

mov cx,inp_buf_length ; CX = max length to read

mov bx,stdin ; BX = standard input handle

mov ah,3fh ; function 3fh = read

int 21h ; transfer to MS-DOS

mov si,offset inp_buf ; fold lowercase characters

mov cx,inp_buf_length ; in entry to uppercase

gcmd1: cmp byte ptr [si],'a' ; check if 'a-z'

jb gcmd2 ; jump, not in range

cmp byte ptr [si],'z' ; check if 'a-z'

ja gcmd2 ; jump, not in range

sub byte ptr [si],'a'-'A' ; convert to uppercase

gcmd2: inc si ; advance through entry

loop gcmd1

ret ; back to caller

get_cmd endp

get_comspec proc near ; get location of COMMAND.COM

; from environment "COMSPEC="

; returns carry = false

; if COMSPEC found

; returns carry = true

; if no COMSPEC

mov si,offset com_var ; DS:SI = string to match...

call get_env ; search environment block

jc gcsp2 ; jump if COMSPEC not found

; ES:DI points past "="

mov si,offset com_spec ; DS:SI = local buffer

gcsp1: mov al,es:[di] ; copy COMSPEC variable

mov [si],al ; to local buffer

inc si

inc di

or al,al ; null char? (turns off carry)

jnz gcsp1 ; no, get next character

gcsp2: ret ; back to caller

get_comspec endp

get_env proc near ; search environment

; call DS:SI = "NAME="

; uses contents of "ENV_SEG"

; returns carry = false and ES:DI

; pointing to parameter if found,

; returns carry = true if no match

mov es,env_seg ; get environment segment

xor di,di ; initialize env offset

genv1: mov bx,si ; initialize pointer to name

cmp byte ptr es:[di],0 ; end of environment?

jne genv2 ; jump, end not found

stc ; no match, return carry set

ret

genv2: mov al,[bx] ; get character from name

or al,al ; end of name? (turns off carry)

jz genv3 ; yes, name matched

cmp al,es:[di] ; compare to environment

jne genv4 ; jump if match failed

inc bx ; advance environment

inc di ; and name pointers

jmp genv2

genv3: ; match found, carry = clear,

ret ; ES:DI = variable

genv4: xor al,al ; scan forward in environment

mov cx,-1 ; for zero byte

cld

repnz scasb

jmp genv1 ; go compare next string

get_env endp

exec proc near ; call MS-DOS EXEC function

; to run COMMAND.COM

mov stkseg,ss ; save stack pointer

mov stkptr,sp

; now run COMMAND.COM

mov dx,offset com_spec ; DS:DX = filename

mov bx,offset par_blk ; ES:BX = parameter block

mov ax,4b00h ; function 4bh = EXEC

; subfunction 0 =

; load and execute

int 21h ; transfer to MS-DOS

mov ax,_DATA ; make data segment

mov ds,ax ; addressable again

mov es,ax

cli ; (for bug in some 8088s)

mov ss,stkseg ; restore stack pointer

mov sp,stkptr

sti ; (for bug in some 8088s)

jnc exec1 ; jump if no errors

; display error message

mov dx,offset msg2 ; DS:DX = message address

mov cx,msg2_length ; CX = message length

mov bx,stderr ; BX = standard error handle

mov ah,40h ; function 40h = write

int 21h ; transfer to MS-DOS

exec1: ret ; back to caller

exec endp

cls_cmd proc near ; intrinsic CLS command

mov dx,offset cls_str ; send the ANSI escape

mov cx,cls_str_length ; sequence to clear

mov bx,stdout ; the screen

mov ah,40h

int 21h

ret

cls_cmd endp

dos_cmd proc near ; intrinsic DOS command

; set null command tail

mov word ptr par_cmd,offset nultail

call exec ; and run COMMAND.COM

ret

dos_cmd endp

exit_cmd proc near ; intrinsic EXIT command

mov ax,4c00h ; call MS-DOS terminate

int 21h ; function with

; return code of zero

exit_cmd endp

_TEXT ends

STACK segment para stack 'STACK' ; declare stack segment

dw 64 dup (?)

STACK ends

_DATA segment word public 'DATA'

commands equ $ ; "intrinsic" commands table

; each entry is ASCIIZ string

; followed by the offset

; of the procedure to be

; executed for that command

db 'CLS',0

dw cls_cmd

db 'DOS',0

dw dos_cmd

db 'EXIT',0

dw exit_cmd

db 0 ; end of table

com_var db 'COMSPEC=',0 ; environment variable

; COMMAND.COM filespec

com_spec db 80 dup (0) ; from environment COMSPEC=

nultail db 0,cr ; null command tail for

; invoking COMMAND.COM

; as another shell

cmd_tail db 0,' /C ' ; command tail for invoking

; COMMAND.COM as a transient

inp_buf db 80 dup (0) ; command line from standard input

inp_buf_length equ $-inp_buf

cmd_tail_length equ $-cmd_tail-1

prompt db cr,lf,'sh: ' ; SHELL's user prompt

prompt_length equ $-prompt

env_seg dw 0 ; segment of environment block

msg1 db cr,lf

db 'Unable to release memory.'

db cr,lf

msg1_length equ $-msg1

msg2 db cr,lf

db 'EXEC of COMMAND.COM failed.'

db cr,lf

msg2_length equ $-msg2

msg3 db cr,lf

db 'No COMSPEC variable in environment.'

db cr,lf

msg3_length equ $-msg3

cls_str db escape,'[2J' ; ANSI escape sequence

cls_str_length equ $-cls_str ; to clear the screen

; EXEC parameter block

par_blk dw 0 ; environment segment

par_cmd dd cmd_tail ; command line

dd fcb1 ; file control block #1

dd fcb2 ; file control block #2

fcb1 db 0 ; file control block #1

db 11 dup (' ')

db 25 dup (0)

fcb2 db 0 ; file control block #2

db 11 dup (' ')

db 25 dup (0)

stkseg dw 0 ; original SS contents

stkptr dw 0 ; original SP contents

_DATA ends

end shell

Figure 12-5. SHELL.ASM: A simple table-driven command interpreter written in Microsoft Macro Assembler.

The SHELL program is table driven and can easily be extended to provide a powerful customized user interface for almost any application. When SHELL takes control of the system, it displays the prompt

sh:

and waits for input from the user. After the user types a line terminated by a carriage return, SHELL tries to match the first token in the line against its table of internal (intrinsic) commands. If it finds a match, it calls the appropriate subroutine. If it does not find a match, it calls the MS-DOS EXEC function and passes the user's input to COMMAND.COM with the /C switch, essentially using COMMAND.COM as a transient command processor under its own control.

As supplied in these listings, SHELL "knows" exactly three internal commands:

Command Action

CLS Uses the ANSI standard control sequence to clear the

display screen and home the cursor.

DOS Runs a copy of COMMAND.COM.

EXIT Exits SHELL, returning control of the system to the

next lower command interpreter.

You can quickly add new intrinsic commands to either the C version or the assembly-language version of SHELL. Simply code a procedure with the appropriate action and insert the name of that procedure, along with the text string that defines the command, into the table COMMANDS. In addition, you can easily prevent SHELL from passing certain "dangerous" commands (such as MKDIR or ERASE) to COMMAND.COM simply by putting the names of the commands to be screened out into the intrinsic command table with the address of a subroutine that prints an error message.

To summarize, the basic flow of both versions of the SHELL program is as follows:

1.The program calls MS-DOS Int 21H Function 4AH (Resize Memory Block) to shrink its memory allocation, so that the maximum possible space will be available for COMMAND.COM if it is run as an overlay. (This is explicit in the assembly-language version only. To keep the example code simple, the number of paragraphs to be reserved is coded as a generous literal value, rather than being figured out at runtime from the size and location of the various program segments.)

2.The program searches the environment for the COMSPEC variable, which defines the location of an executable copy of COMMAND.COM. If it can't find the COMSPEC variable, it prints an error message and exits.

3.The program puts the address of its own handler in the Ctrl-C vector (Int 23H) so that it won't lose control if the user enters a Ctrl-C or a Ctrl-Break.

4.The program issues a prompt to the standard output device.

5.The program reads a buffered line from the standard input device to get the user's command.

6.The program matches the first blank-delimited token in the line against its table of intrinsic commands. If it finds a match, it executes the associated procedure.

7.If the program does not find a match in the table of intrinsic commands, it synthesizes a command-line tail by appending the user's input to the /C switch and then EXECs a copy of COMMAND.COM, passing the address of the synthesized command tail in the EXEC parameter block.

8.The program repeats steps 4 through 7 until the user enters the command EXIT, which is one of the intrinsic commands, and which causes SHELL to terminate execution.

In its present form, SHELL allows COMMAND.COM to inherit a full copy of the current environment. However, in some applications it may be helpful, or safer, to pass a modified copy of the environment block so that the secondary copy of COMMAND.COM will not have access to certain information.