'===========================================================================
' Subject: READ COMMAND LINE ARGUMENTS        Date: 09-12-99 (23:30)       
'  Author: Brian Marstella                    Code: QB, PDS                
'  Origin: brimars@yahoo.com                Packet: DOS.ABC
'===========================================================================
' FileName:  CMDDEMO2.BAS
' FileDate:  09/13/1999
' FileVer:   1.1 - adds args$(0) program path
'    (CMDDEMO.BAS was v1.0, original date 05/23/99)
'
' Version 1.1 returns the running program's path/filename on args$(0), which
'   might be kinda handy if you'd like to create your INI files relative to
'   the path of the program, rather than the directory the user is in.
'
' Purpose:  Demonstrates how to read command-line parameters in and parse
'   them neatly into an array.  Reads the command-line case correctly and
'   strip out command prefixes (- and / usually).  Also retains quoted
'   strings as 1 string, rather than splitting to multiple arguments.
'
' Status:   Public Domain
'
' Language: QB45 (maybe others with some work; will not work with QBasic
'   that comes with DOS unless you have someone's subroutines that will
'   allow you to simulate DEF SEG and CALL INTERRUPT; may not work then)
'
'   Will not work properly from the QB45 programming environment, due to
'   the calls it uses.  It will find the command line entered when starting
'   QB, rather than any other parms.
'
' O/S:      Works with DOS 3.3+, WIN95, WIN98.  Also appears to work fine
'   with DR-DOS 7.0+ and PC-DOS 7.0.  I'm not sure if it will work with
'   WINNT, OS/2, etc., due to the vastly different systems.
'
' Notes:    Well, I actually wrote the subroutine for my own use.  However, I
'   know that I had a great deal of trouble figuring out how to use DEF SEG,
'   call interrupt and some others, so hopefully I've documented this file
'   well enough that it will help someone a little.
'
'   Just so you know, this program is provided with absolutely no warranty
'   of any kind.  I don't guarantee that it will or won't do anything at all.
'
'   If you would like to contact me for clarifications or whatever, you may
'   e-mail me at brimars@yahoo.com.
'
'   I'm sure that many of you who have programmed for years are disgusted
'   by my formatting, extensive commenting, non-cryptic usages, child-like
'   syntax, or whatever.  Sorry.  I wrote this for me and average people
'   who might need to access the command-line.  For C and assembly
'   programmers, yes I know that there are many ways to do this much more
'   cleanly, etc.  Some people don't program in C and assembly, though, and
'   might wish to have a way to do this in a somewhat clearer, more homey
'   atmosphere than C (try to figure out how to get command line parms on
'   your own in C; then try to use them).
'
'   If you have constructive suggestions or find that I've missed an
'   important method that a user can use to crash this, let me know.  I'll
'   try to make changes accordingly.
'
' Oh, also this program does not reference QB.BI; however, you must have
' QB.LIB available to compile.  If QB.LIB is not present, you must expand it
' off of your distribution disks.  Since this file may not have installed
' properly when you installed QB45, make sure you install it in your library
' directory.  This library is absolutely required to compile files using
' the CALL INTERRUPT, CALL ABSOLUTE, etc.

'**************************************
' Precompile instructions
'**************************************

'**************************************
' dynamic simply means that arrays can
' be redimensioned on the fly and
' allows ERASE to destroy arrays and
' free their memory.
'**************************************

    '$DYNAMIC

'**************************************
' Type Declarations
'**************************************

'**************************************
' type reg is required for call
' interrupt
'**************************************

    TYPE reg
        ax AS INTEGER
        bx AS INTEGER
        cx AS INTEGER
        dx AS INTEGER
        bp AS INTEGER
        di AS INTEGER
        si AS INTEGER
        flags AS INTEGER
    END TYPE
  
'**************************************
' Function Declarations
'**************************************

'**************************************
' Subroutine Declarations
'**************************************

    DECLARE SUB getcmdline (argc%, arg$())
    DECLARE SUB interrupt (intnum%, regsin AS reg, regsout AS reg)
    DECLARE SUB main (argc%, arg$())

'**************************************
' Constant Declarations
'**************************************

    CONST DOSINT = &H21
    CONST FALSE = 0
    CONST TRUE = NOT FALSE

'**************************************
' dimension the array to hold command
' line parameters
'**************************************

    DIM args$(0)

'**************************************
' Begin Main Program
'**************************************

    ' the following statement should be your first call prior to the main
    ' program or any functions that attempt to reference commandline parms

    getcmdline argc%, args$()
   
    ' you may safely remove the sub main from the program; it's only function
    ' is to demonstrate the calls...

    main argc%, args$()

'**************************************
' End Main Program
'**************************************

    END

REM $STATIC
SUB getcmdline (argc%, arg$())

'This subroutine returns the command line as entered (case of characters IS
'retained).  Strips normal terminators (/ and -) and single & double quotes.
'Text enclosed in single and double quotes is kept as a continous string.

'**************************************
' dimension working variables as needed
'**************************************

  DIM regs AS reg
  DIM temparg$(0)

'**************************************
' call the routine to get the PSP
' offset address (stores the command
' line parms among other things...)
'**************************************

  regs.ax = &H6200
  CALL interrupt(DOSINT, regs, regs)
  
'**************************************
' now change the working memory segment
' to whatever value was returned in BX
' by the above call.
'**************************************

  DEF SEG = regs.bx

'**************************************
' first get the segment containing the
' environment block for the running
' program (which, happily, contains the
' running program's path in DOS 3.3+)
'
' the segment address is located at the
' word (2 bytes) at 2Ch and 2Dh
'**************************************

  ppseg% = PEEK(&H2D) * 256 + PEEK(&H2C)

'**************************************
' Offset 80h of this segment stores the
' entire length of the parameters
' entered (including the space between
' the program name and the parms!!!),
' so the command length is actually 1
' less than what you are led to believe
' by good sense or experience on other
' systems
'**************************************

  clen% = PEEK(&H80) - 1
  cl$ = ""
   
'**************************************
' if offset &h80 was greater than -1
' then parms were entered; this section
' assembles them into a continuous
' string and notes that the argument
' counter (argc%) is at least 1
'**************************************

  IF clen% > -1 THEN
    FOR xloop% = 0 TO clen%
      cl$ = cl$ + CHR$(PEEK(&H80 + xloop% + 1))
    NEXT xloop%
      argc% = 1
  ELSE

'**************************************
' if the value at offset 80h was 0 then
' no command line parms were entered
' and the argument counter is set to 0
'**************************************

    argc% = 0
  END IF
  
'**************************************
' now change back to the default memory
' segment; otherwise, you'll get tons
' of errors regarding corrupt string
' space and stuff.  Besides, it's good
' practice
'**************************************

  DEF SEG

'**************************************
' begin assembling the arguments by
' first setting up the input array for
' 1 length, initialize it to null, and
' initialize a couple of other vars as
' needed for the subroutine
'**************************************

  REDIM arg$(0 TO argc%)
  c$ = ""
  quotesopen% = FALSE
  
'**************************************
' now start a loop to assemble the
' actual arguments entered into the
' input array area (arg$) (if any
' parameters were entered)
'**************************************
  
  IF argc% > 0 THEN
   
    FOR xloop% = 1 TO clen% + 1
       
'**************************************
' set lc$ (last character) equal to the
' current character c$ then set c$
' equal to the next character in the
' command line string (cl$)
'**************************************

      lc$ = c$
      c$ = MID$(cl$, xloop%, 1)

'**************************************
' the select case determines what to do
' depending on what the next character
' c$ is
'**************************************

      SELECT CASE c$
           
'**************************************
' if c$ is a space, then we first find
' out if quotes have started.  if it's
' part of a quoted string, we simply
' add it to the current parameter and
' go about our business.  however, if
' lc$ (last character) was not a space,
' it's not part of a quoted string, and
' it's not the first character in the
' parameter, then we assume that we
' have ended the current parameter.
'
' if the last character was a space,
' there's nothing that needs to be done
'
' if it's part of a quoted string,
' we've already taken care of it
'
' if it's the first character in the
' parms, then there's no sense making
' it a part of the current parm
'
' as an afterthought, we also check if
' the last character was a terminator.
' if so, this evaluation has no meaning
'**************************************

        CASE " "
          IF quotesopen% = TRUE THEN
            arg$(argc%) = arg$(argc%) + c$
          END IF
          IF (lc$ <> " " AND lc$ <> "-" AND lc$ <> "/" AND quotesopen% = FALSE AND xloop% > 1) THEN
                  
'**************************************
' if we made it this far, then we've
' reached the end of the current parm
' and need to prep for the next one
' first we redimension a temporary
' array to hold all the current parms,
' since we can't redimension the actual
' parm array (arg$) without destroying
' the parms we already collected
'**************************************

            REDIM temparg$(0 TO argc%)
                   
'**************************************
' now we copy all the parms from the
' parm array arg$ to the temporary
' array temparg$
'**************************************

            FOR yloop% = 1 TO argc%
              temparg$(yloop%) = arg$(yloop%)
            NEXT yloop%
                   
'**************************************
' increment the argument counter (good
' a place as any to do it...)
'**************************************

            argc% = argc% + 1
                   
'**************************************
' now that all the parms are in a temp
' array, we'll redimension our actual
' array arg$ to the new number of args
' expected and then we'll copy the
' parms back using the same method as
' above
'**************************************

            REDIM arg$(0 TO argc%)
            FOR yloop% = 1 TO argc% - 1
              arg$(yloop%) = temparg$(yloop%)
            NEXT yloop%

'**************************************
' next line initializes the current
' arg to NUL and the following lines
' dimension the temporary array back
' to zero to free memory
'**************************************

            arg$(argc%) = ""
            ERASE temparg$
            DIM temparg$(0)
            firstchar% = TRUE
          END IF

'**************************************
' if the character in c$ is a single '
' or double " quote, then we change the
' status of quotesopen%.  this value is
' used simply to track whether we are
' in the middle of a text string (TRUE)
' or not (FALSE)
'**************************************

        CASE CHR$(34), CHR$(39)
          IF quotesopen% = FALSE THEN
            quotesopen% = TRUE
          ELSE
            quotesopen% = FALSE
          END IF

'**************************************
' the next case checks parm terminators
' which I simply strip unless they are
' part of a text string (quotesopen% is
' TRUE or the length of the parm is
' greater than zero).  you can add more
' terminators or other characters to
' strip simply by adding them to the
' case statement separating each by a
' comma.  make sure you also add them
' to the "space" evaluation case above
'**************************************

        CASE "-", "/"
          IF quotesopen% = TRUE OR LEN(arg$(argc%)) > 0 THEN
            arg$(argc%) = arg$(argc%) + c$
          END IF
           
'**************************************
' if it's any other character, we just
' tack it onto the end of the command
' string and go on about our merry way
'**************************************

        CASE ELSE
          arg$(argc%) = arg$(argc%) + c$
       
      END SELECT

    NEXT xloop%
  
'**************************************
' if the last argument is empty (which
' means that your user entered spaces
' or terminators at the end of the
' command line), then we kill it much
' the same way we added to the array.
' we also subtract 1 from the argument
' counter
'**************************************

    IF LEN(arg$(argc%)) = 0 THEN
      argc% = argc% - 1
       
'**************************************
' if we no longer have any parms left
' then we can simply set the counter to
' zero, destroy the argument array
'**************************************

      IF argc% = 0 THEN
        REDIM arg$(0)
      END IF
       
'**************************************
' continue copying the array
'**************************************

      REDIM temparg$(0 TO argc%)
      FOR xloop% = 1 TO argc%
        temparg$(xloop%) = arg$(xloop%)
      NEXT xloop%
      REDIM arg$(0 TO argc%)
      FOR xloop% = 1 TO argc%
        arg$(xloop%) = temparg$(xloop%)
      NEXT xloop%
      ERASE temparg$
      REDIM temparg$(0)
    END IF

  END IF

'**************************************
' now let's get the program's path
' info; 1st change the segment to the
' program's environment segment
'**************************************

  DEF SEG = ppseg%

'**************************************
' start looking through the data here
' until you locate a byte with 00h in
' it; this marks the end of the
' environment strings and the beginning
' of the program path (well, see the
' actual code comments...)
'**************************************

  xcount% = 0
  done% = 0
  DO WHILE done% = 0
    xcount% = xcount% + 1

'**************************************
' if the next values in the string are
' both &h00, that means that you are
' near the program path (4 bytes away)
'**************************************

    IF (PEEK(xcount%) = &H0) AND (PEEK(xcount% + 1) = &H0) THEN

'**************************************
' increment the offset count by 4 bytes
'**************************************

      xcount% = xcount% + 4
      nextchar% = PEEK(xcount%)
      arg$(0) = ""

'**************************************
' get the program path; &h00 follows
' the path; we'll set the done% flag
' when we hit the &h00
'**************************************

      DO
        arg$(0) = arg$(0) + CHR$(nextchar%)
        xcount% = xcount% + 1
        nextchar% = PEEK(xcount%)
      LOOP WHILE nextchar% <> &H0
      done% = 1
    END IF
  LOOP

'**************************************
' finally, change back to the main
' data segment
'**************************************

  DEF SEG

'**************************************
' set c$ and lc$ = NULL to kill the
' space they're taking up
'**************************************

  c$ = ""
  lc$ = ""

END SUB

SUB main (argc%, args$())

' Sample subroutine that uses the arguments found on the command line.

  CLS
  PRINT "The program path is: "; args$(0)
  PRINT
  PRINT "Total Arguments: "; argc%
  PRINT
  IF argc% > 0 THEN
    FOR xloop% = 1 TO argc%
      PRINT TAB(5); "Argument #"; xloop%; " = "; args$(xloop%)
    NEXT xloop%
  END IF
    
END SUB
