' QBasic Beautifier by  Kewbie_newbie, found at Aliphas page
'
' THIS PROGRAM USED FEATURES AVAILABLE ONLY IN QuickBasic 7.1 (as far as
'   I know) (Tested in Qb 4.5 by Antoni Gual)
'
'  The 'Quick Basic Beautifier' program performs the following actions:
'
'    1.  Indents program lines according to control flow structures
'    2.  Removes stupid, useless, unnecessary 'LET' statements (unless
'        overridden)
'    3.  Changes 'REM' keyword to ' (unless overridden)
'    4.  Optionally re-aligns trailing comments
'    5.  Optionally removes all comments
'    6.  Optionally allows a left margin and/or line length to be set (for
'        output to a printer, perhaps
'
'                         Quick Basic Beautifier
'
' Usage:
'
'  qbb InFile[.Ext] [OutFile[.Ext]] [/C:A|B|#] /F [/L] [/M:#[,#]] [/R:?] [/T:#]
'   [/Q]
'
'   File extentions are optional.  If the InFile extension is omitted,
'   'BAS' is assumed.  If the OutFile extension is omitted, 'QBB' is
'   assumed.  If the OutFile is omitted, 'InFile.QBB' is assumed.
'   If the OutFile starts with a '.', OutFile will being 'InFile.<specified ext>'.
'
'   The colons on the switches are optional.  Switches are not case sensitive
'
'   /C:A|B|T - move trailing comments to the line above or below.
'              Default = leave trailing
'   /C:#     - align trailing comments to column #
'              (See note below)
'   /F       - include OPEN/CLOSE in nesting
'              (a word of caution: MOST of the time, you probably will NOT
'               be happy with the results of using this switch)
'   /L       - do not remove useless 'LET' statements.
'              Default = whack 'em
'   /M:#[,#] - left Margin width [and line length] (possibly to format
'              output for a printer -- see note below)
'              Default = leave as is
'   /R:?     - controls comment keyword.
'      '     - use ' keyword (default)
'      N     - do not change keyword
'      D     - delete all comments
'   /S:L     - keep only 'looping' statements on a single line
'              ('LOOPING' mode - see note below)
'   /S:#     - controls handling of multi-statement lines
'              # is the number of statements to keep on a line
'              Default = 0 (no multi-statement line splitting)
'              1 = put every statement on a separate line
'              2 - infinity = leave up to 'n' statements on a line
'              (see note below)
'   /T:#     - # is the number of columns per tab.
'              Default = 2 columns
'   /Q       - quiet mode, no info or pause to reflect, just do it
'
' Notes:
'
'   The input to QBB must be parsable by qbasic, and the keywords must be
'   in upper case.  If you download something that was entered in lower
'   case, load and save it with qbasic first to adjust the case of the
'   keywords
'
'   I have included a new feature in this version: if there is a particular
'   sequence of switches that you always use, say "/T3 /C40 /Q", you can
'   set the environment variable "QBBOPT" to specify those switches and
'   QBB will operate as if those switches had been specified at invocation.
'   These default settings can be overridden.  Simply specify a new value
'   on the command line when invoking QBB.  The /F /L and /Q switches
'   act as toggles, so to turn off /Q in the above example, to turn quiet
'   mode OFF, you would invoke QBB with "QBB infile.bas outfile.qbb /Q".
'   And of course you can add additional switches if you need to
'
'   The input and output files can not be the same.  QBB checks for this
'   and wont allow it
'
'   If a comment column is specified and a line is too long, the trailing
'   comment will be separated from the end of the line by one space
'
'   If a line length is specified (with or without a margin), comment
'   alignment is forced to "ABOVE".  It just made things easier. Also,
'   the output will almost certainly not interpret or compile properly,
'   I added this feature only because a printer I sometimes use doesn't
'   wrap long lines to the next line, it truncates them!  (I use it be-
'   cause it's FAST.  A couple of people have asked me 'How fast?'.  It
'   was a laser printer that was channel attached to an IBM mainframe,
'   and at a guess, I'd say it printed 80 to 100 pages a minute.  All I
'   can tell you is that the paper went by too fast for me to be able
'   to read.  Alas, it wasn't being used enough to justify keeping it,
'   so it was taken away :(..... )
'
'   Qbasic breaks long program lines into shorter pieces and stores them
'   in the source file with a continuation character at the end (" _"). QBB
'   concatenates these continued lines together in the output file.  The
'   first time the program is saved by qbasic, it will store the lines the
'   way it was designed to.  If you load a source file that has one of
'   these long lines and try to compile it under qbasic 4.5 or 7.1 (the
'   only ones I've tried, it will fail with 'line too long' errors, and
'   other errors probably unrelated to any actual errors in the program.
'   To fix this, save the program before compiling
'
'   The output is indented the way I like it!
'
'   If the /S switch is omitted, or '/S:0' is specified, QBB will handle
'   multiple statement lines as it did prior to this version, i.e., not
'   very well.  Especially if there are embeded structuring keywords.  It
'   does handle a few common cases
'
'   With the addition of the '/S' switch, QBB now handles multiple state-
'   ment lines more effectively.  There are essentially ?? modes for split-
'   ting lines with multiple statments.  'LOOPING' mode causes QBB to
'   leave multiple statements on lines that have both a 'DO' and a 'LOOP'
'   statement, or 'FOR' and 'NEXT' or 'WHILE' and 'WEND'.
'
'   qbasic always seems to move labels to column 1.  QBB will indent any
'   labels which are placed inside control-flow structures.  qbasic will
'   move them to column 1 the first time the program is loaded
'
'   All three qbasic help files indicate that a label can have blanks
'   and tabs between the label and the colon.  When such a line is
'   added to a program, qbasic moves the colon to the end of the label.
'   QBB always places the colon at the end of the label
'
' "NO PROBLEM IS INSOLUBLE IN ALL CONCIEVABLE CIRCUMSTANCES"
' (Cosmic AC in "The Last Question" by Isaac Asimov)

BeginProgram:

' This function is executed once at the beginning of the program to determine
'   input and output files, and other command line switches described above
'   to control source formatting
DECLARE SUB ParseInputParameters ()

' These two functions are used during input parameter parsing for extracting
'   file names and parameter values
DECLARE FUNCTION GetParm$ (parm$)
DECLARE FUNCTION GetFile$ ()

' If the user doesn't know what to do, this subroutine will give him/her
'   some SIMPLIFIED instructions
DECLARE SUB Usage ()

' All but one of the "do" functions are the handlers for the named qbasic
'   program flow keywords.
' Two of these functions handle two keywords:
'   doCASEELSE and doFUNCSUB.
' All of these functions deal with input and output via 'global' variables
DECLARE SUB doCASEELSE ()
DECLARE SUB doDECLARE ()
DECLARE SUB doDEFFN ()
DECLARE SUB doDO ()           ' be careful not to step in it!
DECLARE SUB doENDSEL ()
DECLARE SUB doFOR ()
DECLARE SUB doFUNCSUB ()
DECLARE SUB doIF ()
DECLARE SUB doNEXT ()
DECLARE SUB doSELECT ()
DECLARE SUB doTYPE ()
DECLARE SUB doWHILE ()

' This one "do" function does not handle a qbasic keyword.  It controls the
'   level of indentation by manipulating 'global' variables.  It's counter-
'   part is "unNest"
DECLARE SUB doNest ()
DECLARE SUB unNest ()

' This function takes care of putting the formatted source code to the
'   appropriate output device
DECLARE SUB PrintLine (Text$)

' This function looks for and removes "LET" statements
DECLARE FUNCTION DeLETify$ (Text$)

' This function looks for comments
DECLARE FUNCTION FindComment% (Text$)

' This function returns a boolean value to indicate whether or not a given
'   token is a qbasic keyword
DECLARE FUNCTION KeyWord% (Text$)

' This function breaks each line up into individual tokens
DECLARE FUNCTION LocateKeyWord% (Text$, CmntKyWd$)

' These two functions and a sub form the main loop that reads the input file
'   and produces the output file.  The functions are both boolean
DECLARE FUNCTION ParseProgramLine% ()
DECLARE FUNCTION StructureProgramLine% ()
DECLARE SUB PrintProgramLine ()

' This function removes leading whitspace characters from the input source
'   lines
DECLARE FUNCTION TrimWhite$ (Text$)

' This function is simply a loop that reads data into the "Structure$" array
DECLARE SUB SetStructure (Max%)

' This function is simply a loop that reads data into the "Reserved$" array
DECLARE SUB SetReserved ()

' The following five constants deal with comment handling
CONST CmtABOVE% = -1
CONST CmtBELOW% = -2
CONST CmtASIS% = 2
CONST CmtNONE% = 1
CONST CmtTICK% = 3

' There are twenty-eight program control flow statements handled by QBB
CONST MaxStruct% = 28

' This program uses nineteen 'global' variables
' Nine of them are integer variables
DIM SHARED CmtCol%
DIM SHARED CmtKey%
DIM SHARED Length%

DIM SHARED Margin%
DIM SHARED TabWidth%
DIM SHARED TabLevel%

DIM SHARED CmtFlag%
DIM SHARED LetFlag%
DIM SHARED PrintFlag%

' Eight of them are string variables
DIM SHARED Cmd$           ' working copy of COMMAND$
DIM SHARED Comment$
DIM SHARED InputStatement$
DIM SHARED Margin$
DIM SHARED Statement$
DIM SHARED TabLevel$

DIM SHARED iFile$
DIM SHARED oFile$

DIM SHARED UserNames$

' Two of them are string arrays
DIM SHARED Reserved$(ASC("A") TO ASC("Z"))
DIM SHARED Structure$(0 TO MaxStruct%, 1 TO 2)

' Enough with the preparation stuff, time to start really doing something
CLS : PRINT                   ' clean up any leftover junk

ParseInputParameters          ' find out what the user wants to do

ON ERROR GOTO MissingInput    ' if the user goofed, try to tell him
                              '   what he did
OPEN iFile$ FOR INPUT AS #1   ' if you don't know what this does, this
                              '   program is probably too complex for you
ON ERROR GOTO 0               ' don't try to be too helpful
OPEN oFile$ FOR OUTPUT AS #2  ' ditto input comment

SetReserved                   ' populate the reserved word array
SetStructure (MaxStruct%)     ' populate the program control keyword/
                              '   handler routine array
TabLevel% = 0                 ' start out at left margin
TabLevel$ = ""
UserNames$ = " "              ' clear the "symbol table"

' This is the main program loop.  It loops through all of the input source
'   file lines.  Originally, the code for the two functions and subroutine
'   was included in this loop.  It wasn't a huge loop, but it took more that
'   one screen, so I decided to break it down according to what was happen-
'   ing functionally.  In the original loop, PrintProgramLine was inside
'   StructureProgramLine, which was inside ParseProgramLine (as I remember
'   it, that is).
DO UNTIL EOF(1) AND InputStatement$ = ""
  IF ParseProgramLine% THEN IF StructureProgramLine% THEN PrintProgramLine
LOOP

' Since the screen was cleared, remind the user of what s/he did
PRINT CURDIR$ + ">" + COMMAND$
END ' all done

' Error handling routine for missing input file
MissingInput:
PRINT "Input file not found"
END

' The following data statements contain all of the reserved words for
'   qbasic 1.1, QuickBasic 4.5 and QuickBasic 7.1
'
ReservedData:
DATA " ABS ABSOLUTE ACCESS ALIAS ALL AND ANY APPEND AS ASC ATN "
DATA " BASE BASIC BEEP BEGINTRANS BINARY BLOAD BOF BSAVE BYVAL "
DATA " CALL CALLS CASE CCUR CDBL CDECL CHAIN CHDIR CHDRIVE CHECKPOINT CINT CIRCLE CLEAR CLNG CLOSE CLS COLOR COM COMMITTRANS COMMON CONST COS CREATEINDEX CSNG CSRLIN CURRENCY CVC CVD CVDMBF CVI CVL CVS CVSMBF "
DATA " DATA  DECLARE DEF DEFCUR DEFDBL DEFINT DEFLNG DEFSNG DEFSTR DELETE DELETEINDEX DELETETABLE DIM DO DOUBLE DRAW "
DATA " ELSE ELSEIF END ENDIF ENVIRON EOF EQV ERASE ERDEV ERL ERR ERROR EVENT EXIT EXP "
DATA " FIELD FILEATTR FILES FIX FN FOR FRE FREEFILE FUNCTION "
DATA " GET GO GOSUB GOTO "
DATA " -H- "
DATA " IF IMP INP INPUT INSERT INSTR INT INTEGER INTERRUPT IOCTL IS ISAM "
DATA " -J- "
DATA " KEY KILL "
DATA " LBOUND LEN LET LINE LIST LOC LOCAL LOCATE LOCK LOF LOG LONG LOOP LPOS LPRINT LSET "
DATA " MKDIR MOD MOVEFIRST MOVELAST MOVENEXT MOVEPREVIOUS "
DATA " NAME NEXT NOT "
DATA " OFF ON OPEN OPTION OR OUT OUTPUT "
DATA " PAINT PALETTE PCOPY PEEK PEN PLAY PMAP POINT POKE POS PRESET PRINT PSET PUT "
DATA " -Q- "
DATA " RANDOM RANDOMIZE READ REDIM REM RESET RESTORE RESUME RETRIEVE RETURN RMDIR RND ROLLBACK RSET RUN "
DATA " SADD SAVEPOINT SCREEN SEEK SEEKEQ SEEKGE SEEKGT SEG SELECT SETINDEX SETMEM SGN SHARED SHELL SIGNAL SIN SINGLE SLEEP SOUND SPC SQR SSEG SSEGADD STACK STATIC STEP STICK STOP STRIG STRING SUB SWAP SYSTEM "
DATA " TAB TAN THEN TIMER TO TROFF TRON TYPE "
DATA " UBOUND UEVENT UNLOCK UNTIL UPDATE USING "
DATA " VAL VARPTR VARSEG VIEW "
DATA " WAIT WEND WHILE WIDTH WINDOW WRITE "
DATA " XOR "
DATA " -Y- "
DATA " -Z- "

' The following data statements are used to build the
'   "program control flow keyword/handler function" table
StructureData:
DATA "","NoS"
DATA "DATA" , "NoS"
DATA "END IF" , "unN"
DATA "ELSE" , "doC"
DATA "ELSEIF" , "doC"
DATA "IF" , "doI"
DATA "LOOP" , "unN"
DATA "EXIT DO" , "NoS"
DATA "DO" , "doDO"
DATA "NEXT" , "doN"
DATA "EXIT FOR" , "NoS"
DATA "FOR" , "doFO"
DATA "END SELECT" , "doE"
DATA "SELECT" , "doS"
DATA "CASE" , "doC"
DATA "END TYPE" , "unN"
DATA "TYPE" , "doT"
DATA "END FUNCTION" , "unN"
DATA "EXIT FUNCTION" , "NoS"
DATA "DECLARE FUNCTION" , "doDEC"
DATA "FUNCTION" , "doFU"
DATA "END SUB" , "unN"
DATA "EXIT SUB" , "NoS"
DATA "DECLARE SUB" , "doDEC"
DATA "SUB" , "doFU"
DATA "END DEF" , "unN"
DATA "EXIT DEF" , "NoS"
DATA "DEF FN" , "doDEF"
DATA "WEND" , "unN"
DATA "WHILE" , "doW"

FUNCTION DeLETify$ (Text$)
  Done% = 0
  Work$ = " " + Text$

  DO
    LetLoc% = LocateKeyWord%(Work$, "LET ")

    IF LetLoc% THEN
      Work$ = LEFT$(Work$, LetLoc% - 1) + MID$(Work$, LetLoc% + 4)
    ELSE
      Done% = -1
    END IF
  LOOP UNTIL Done%

  DeLETify$ = MID$(Work$, 2)
END FUNCTION

SUB doCASEELSE
  IF LEFT$(Statement$, 4) = "ELSE" OR LEFT$(Statement$, 6) = "ELSEIF" OR LEFT$(Statement$, 4) = "CASE" THEN
    IF TabLevel% THEN
      unNest
      PrintProgramLine
      doNest
      PrintFlag% = 0
    END IF
  END IF
END SUB

SUB doDECLARE
  nSta% = INSTR(10, Statement$, " ") + 1
  nEnd% = INSTR(nSta%, Statement$, " ")
  UserNames$ = UserNames$ + MID$(Statement$, nSta%, nEnd% - nSta%) + " "
END SUB

SUB doDEFFN
  IF INSTR(Statement$, "=") = 0 THEN
    PrintProgramLine
    doNest
    PrintFlag% = 0
  END IF
END SUB

SUB doDO
  IF INSTR(Statement$, "LOOP") = 0 THEN
    PrintProgramLine
    doNest
    PrintFlag% = 0
  END IF
END SUB

SUB doENDSEL
  unNest
  unNest
END SUB

SUB doFOR
  IF INSTR(Statement$, "NEXT") THEN EXIT SUB
  opn% = INSTR(Statement$, "OPEN")
  IF opn% AND opn% < INSTR(Statement$, "FOR") THEN EXIT SUB

  PrintProgramLine
  doNest
  PrintFlag% = 0
END SUB

SUB doFUNCSUB
  TabLevel$ = ""
  PrintProgramLine
  TabLevel% = TabWidth%
  TabLevel$ = STRING$(TabLevel%, " ")
  PrintFlag% = 0
END SUB

SUB doIF
  IF RIGHT$(Statement$, 4) = "THEN" THEN
    PrintProgramLine
    doNest
    PrintFlag% = 0
  END IF
END SUB

SUB doNest
  TabLevel% = TabLevel% + TabWidth%
  TabLevel$ = STRING$(TabLevel%, " ")
END SUB

SUB doNEXT
' This subroutine is called when QBB encounters the 'NEXT'
'   keyword in the source code it's formatting
' It causes the level of indentation to be decreased by the
'   appropriate number of tabs

  ' Frankly, I don't know why this check is in here.  This
  '   subroutine SHOULD only get called when 'NEXT' is found
  '   I can only guess that at some point in my 'code as you
  '   go' design, I found it necessary to put this here to
  '   handle some situation
  IF LEFT$(Statement$, 4) = "NEXT" THEN
    unNest  ' <-- performs the actual indent level adjustment

    ' Check for multi-statement line.  I cheated and misused Comma% to save
    '  creating an additional variable
    Comma% = INSTR(Statement$, ":")
    ' Only concerned with commas associated with the 'NEXT'
    IF Comma% THEN Text$ = LEFT$(Statement$, Comma% - 1) ELSE Text$ = Statement$

    ' This loop takes care of programs that do nested
    ' FOR/NEXT loops like:
    ' FOR Idx = 1 TO 5
    '   FOR Jdx = 1 TO 5
    '     A$(Idx, Jdx) = ""
    ' NEXT Jdx, Idx
    ' Personally, I never code this way, I prefer to use discrete 'NEXT'
    ' statements.
    Comma% = INSTR(Text$, ",") ' look for a comma
    DO WHILE Comma%            ' if none were found, loop is skipped
      unNest                   '   otherwise adjust indent for each var on
                               '   the 'NEXT' statement.  Remember that by
                               '   the time this loop is reached, the 1st
                               '   variable is already accounted for
      Comma% = INSTR(Comma% + 1, Text$, ",") ' find the next comma
      ' 'INSTR' returns zero (false) when it doesn't find the text being
      ' searched for, so after the last comma is found, the loop ends
    LOOP
  END IF
END SUB

SUB doSELECT
  PrintProgramLine
  doNest
  doNest
  PrintFlag% = 0
END SUB

SUB doTYPE
  PrintProgramLine
  doNest
  PrintFlag% = 0
END SUB

SUB doWHILE
  IF LEFT$(Statement$, 5) = "WHILE" THEN
    PrintProgramLine
    doNest
    PrintFlag% = 0
  END IF
END SUB

FUNCTION FindComment% (Text$)
  CmntREM% = LocateKeyWord%(Text$, "REM ")
  CmntQUO% = LocateKeyWord%(Text$, "'")

  IF CmntQUO% OR CmntREM% THEN
    IF CmntQUO% > 0 AND CmntREM% > 0 THEN
      IF CmntQUO% < CmntREM% THEN
        FindComment% = CmntQUO%
      ELSE
        FindComment% = -CmntREM%
      END IF
    ELSE
      FindComment% = CmntQUO% + -CmntREM%
    END IF
  ELSE
    FindComment% = 0
  END IF
END FUNCTION

FUNCTION GetFile$
' This function looks for a file name in the command line parameters

  pSpace% = INSTR(Cmd$, " ")        ' parms are separated by spaces
                                    ' the input parms are set up so
                                    ' there'll always be a space
  File$ = LEFT$(Cmd$, pSpace% - 1)  ' get the file name (and possible
                                    ' extension
  IF LEFT$(File$, 1) <> "/" THEN    ' is it a switch?
    Cmd$ = MID$(Cmd$, pSpace% + 1)  ' no, so gobble this file name
    GetFile$ = File$                ' set the return value
  ELSE                              ' it wasn't a file name, so
    GetFile$ = ""                   ' return null
  END IF
END FUNCTION

FUNCTION GetParm$ (parm$)
' This function returns the value associated with parms passed on the command
'   line
'   All parameters start with a '/'
'   The parm name MAY end with a colon
'   The parm value MUST be terminated by a space or newline
  pParm% = INSTR(Cmd$, "/" + parm$)

  IF pParm% THEN
    pSpace% = INSTR(pParm%, Cmd$, " ")
    Temp$ = MID$(Cmd$, pParm% + 2, pSpace% - (pParm% + 2))
    IF LEFT$(Temp$, 1) = ":" THEN Temp$ = MID$(Temp$, 2)
    IF Temp$ = "" THEN Temp$ = " "
  ELSE
    Temp$ = ""
  END IF

  GetParm$ = Temp$
END FUNCTION

FUNCTION KeyWord% (Text$)
  Char% = INSTR(UserNames$, " " + Text$ + " ") <> 0

  IF NOT Char% THEN
    Char% = ASC(Text$)

    IF Char% >= ASC("A") AND Char% <= ASC("Z") THEN
      KeyWord% = INSTR(Reserved$(Char%), " " + Text$ + " ") <> 0
    ELSE
      KeyWord% = 0
    END IF
  ELSE
    KeyWord% = -1
  END IF
END FUNCTION

FUNCTION LocateKeyWord% (Text$, QBKyWd$)
  Last% = 0
  Known% = 0

  DO
    QBKeyWd% = INSTR(Last% + 1, Text$, QBKyWd$)
    Last% = QBKeyWd%

    IF QBKeyWd% THEN
      Done% = 0
      lq% = 0
      DO
        fq% = INSTR(lq% + 1, Text$, CHR$(34))
        IF fq% AND fq% < QBKeyWd% THEN ' found ", gotta check
          lq% = INSTR(fq% + 1, Text$, CHR$(34))
          IF lq% > QBKeyWd% THEN
            QBKeyWd% = 0
            Done% = -1
          END IF
        ELSE ' no "s or "s >> ', it's a QBasci KeyWord
          Done% = -1
          Known% = -1
        END IF
      LOOP UNTIL Done%
    ELSE
      Known% = -1
    END IF
  LOOP UNTIL Known%

  LocateKeyWord% = QBKeyWd%
END FUNCTION

SUB ParseInputParameters
' This subroutine figures out what the user wants qbb to do for him/her

  Cmd$ = COMMAND$ + "    " ' get a working copy of the input parameters,
                           '   adding a trailing blank
  Temp$ = GetFile$         ' the 1st parm is the input file
                           ' GetFile gobbles file names
  IF Temp$ = "" THEN Usage ' if no parms, tell 'em how to use qbb
                           ' (the program will exit after displaying usage

  iFile$ = Temp$           ' got a good file name so
  dot% = INSTR(Temp$, ".") ' look for the extension

  IF dot% <> 0 THEN                 ' if there was an extension,
    Temp$ = LEFT$(Temp$, dot% - 1)  ' get the base file name
  ELSE
    iFile$ = iFile$ + ".bas"        ' otherwise add the default input ext
  END IF

  oFile$ = Temp$ + ".qbb"           ' preset the default output file name,
                                    ' just in case it wasn't given
  oName$ = Temp$                    ' save the base name, just in case only
                                    ' an extension is given
  Temp$ = GetFile$                  ' see if an output file was given
  IF Temp$ <> "" THEN               ' if it was,
    oFile$ = Temp$                  ' set the output file name
  END IF
  dot% = INSTR(oFile$, ".")         ' look for the extension
  IF dot% = 0 THEN                  ' if there wasn't one,
    oFile$ = oFile$ + ".qbb"        ' add the default
  ELSEIF dot% = 1 THEN              ' but if ONLY an extension was given
    oFile$ = oName$ + oFile$        ' make the name the input file base name
  END IF                            ' and the output file extension

  ' make sure that two different files were specified for input and output
  IF UCASE$(iFile$) = UCASE$(oFile$) THEN
    PRINT "The output file can not be the same as the input file."
    END
  END IF

  ' now start parsing the rest of the input parameters (switches)

  ' The comment switch /C[:#]:
  Temp$ = GetParm$("C")
  IF LEFT$(Temp$, 1) < "A" THEN
    CmtCol% = VAL(Temp$)
  ELSE
    CmtCol% = (ASC("A") - 1) - ASC(Temp$)
  END IF
  IF CmtCol% < CmtBELOW% THEN CmtCol% = 0
  CmtCol$ = LEFT$(Temp$, 1)

  '/L:
  LetFlag% = GetParm$("L") = ""

  '/M:#[,#]
  Temp$ = GetParm("M")
  IF Temp$ <> "" THEN
    Comma% = INSTR(Temp$, ",")
    IF Comma% THEN
      Margin% = VAL(LEFT$(Temp$, Comma% - 1))
      Length% = VAL(MID$(Temp$, Comma% + 1))
    ELSE
      Margin% = VAL(Temp$)
      Length% = 0
    END IF
  ELSE
    Margin% = 0
    Length% = 0
  END IF
  IF Length% <= 0 THEN
    Length% = 0
  ELSE
    CmtCol% = CmtABOVE%
    CmtCol$ = "A"
  END IF
  IF Margin% < 0 THEN Margin% = 0
  Margin$ = SPACE$(Margin%)

  '/R:?:
  Temp$ = GetParm("R")
  IF Temp$ <> "" THEN CmtKey% = INSTR("DN'", GetParm("R")) ELSE CmtKey% = CmtTICK%

  '/T:#:
  TabWidth% = VAL(GetParm$("T"))
  IF TabWidth% < 1 THEN TabWidth% = 2
  IF TabWidth% > 12 THEN TabWidth% = 12

  IF GetParm("Q") <> " " THEN ' if a stand-alone parameter (/q) is entered
    IF CmtCol% >= 0 THEN      ' GetParm returns a space, otherwise a null $
      CmtCol$ = LTRIM$(STR$(CmtCol%))
    ELSEIF CmtCol$ = "A" THEN
      CmtCol$ = "ABOVE"
    ELSE
      CmtCol$ = "BELOW"
    END IF

    DIM CmtKey$(1 TO 3)

    CmtKey$(1) = "REMove"
    CmtKey$(2) = "LEAVE 'AS IS'"
    CmtKey$(3) = "CONVERT REM TO '"

    IF Length% > 0 THEN Length$ = "" ELSE Length$ = " (FULL LENGTH)"

    IF LetFlag% THEN LetFlag$ = "TRUE" ELSE LetFlag$ = "FALSE"

    PRINT " input file: '"; UCASE$(iFile$); "'"
    PRINT "output file: '"; UCASE$(oFile$); "'"
    PRINT "comment col: "; CmtCol$
    PRINT "comment key: "; CmtKey$(CmtKey%)
    PRINT "line length:"; Length%; Length$
    PRINT "     margin:"; Margin%
    PRINT "remove LETs: "; LetFlag$
    PRINT "  tab width:"; TabWidth%
    PRINT

    Temp$ = ""
    PRINT "Proceed (Y/N)? ===> ";
    WHILE Temp$ = "": Temp$ = UCASE$(INKEY$): WEND
    PRINT Temp$
    IF Temp$ <> "Y" THEN END
  END IF
END SUB

FUNCTION ParseProgramLine%

  PrintFlag% = -1

  IF InputStatement$ = "" THEN
    LINE INPUT #1, InputStatement$
    DO WHILE RIGHT$(InputStatement$, 2) = " _"
      LINE INPUT #1, Continue$
      InputStatement$ = LEFT$(InputStatement$, LEN(InputStatement$) - 1) + TrimWhite$(Continue$)
    LOOP
    InputStatement$ = TrimWhite$(InputStatement$) 'get rid of any existing indentation
  END IF

  ColonLoc% = LocateKeyWord%(InputStatement$, ":")

  IF ColonLoc% = 0 OR LEFT$(InputStatement$, 3) = "IF " OR LEFT$(InputStatement$, 1) = "'" OR LEFT$(InputStatement$, 7) = "ELSEIF " OR LEFT$(InputStatement$, 4) = "REM " THEN
    Statement$ = InputStatement$
    InputStatement$ = ""
  ELSE
    Statement$ = TrimWhite$(LEFT$(InputStatement$, ColonLoc% - 1))
    InputStatement$ = TrimWhite$(MID$(InputStatement$, ColonLoc% + 1))

    PRINT
    PRINT ColonLoc%
    PRINT Statement$
    PRINT InputStatement$
    'WHILE INKEY$ = "": WEND
  END IF

  Comment$ = ""
  Cmnt% = FindComment%(Statement$) ' look for comments
  CmtFlag% = ABS(Cmnt%) = 1

  IF CmtFlag% AND CmtKey% = CmtNONE% THEN
    PrintFlag% = 0
  ELSE
    IF Cmnt% THEN ' there's a comment
      RemFlag% = Cmnt% < 0
      Cmnt% = ABS(Cmnt%)

      IF CmtKey% <> CmtNONE% THEN
        Comment$ = MID$(Statement$, Cmnt%)
        IF RemFlag% AND CmtKey% <> CmtASIS% THEN Comment$ = "'" + MID$(Comment$, 4)' convert "REM" to "'"
        IF Cmnt% = 1 THEN
          Statement$ = Comment$
          Comment$ = ""
          Cmnt% = LEN(Statement$) + 1
        END IF
      END IF
    ELSE
      Cmnt% = LEN(Statement$) + 1
    END IF

    Statement$ = RTRIM$(LEFT$(Statement$, Cmnt% - 1))
    DO WHILE RIGHT$(Statement$, 1) = ":"
      Statement$ = RTRIM$(LEFT$(Statement$, LEN(Statement$) - 1))
    LOOP
    IF Statement$ <> "" AND LEFT$(Statement$, 1) <> "'" AND INSTR(Statement$, " ") = 0 THEN IF NOT KeyWord%(Statement$) THEN Statement$ = Statement$ + ":"
  END IF

  ParseProgramLine% = PrintFlag%
END FUNCTION

SUB PrintLine (Text$)
  CmntKey$ = LEFT$(Text$, 3)
  IF CmntKey$ = "REM" OR LEFT$(CmntKey$, 1) = "'" THEN
    IF LEFT$(CmntKey$, 1) = "'" THEN CmntKey$ = "'"
  ELSE
    CmntKey$ = ""
  END IF
  CmntSplit$ = ""

  MinLen% = LEN(Margin$ + TabLevel$) + 1

  IF Length% = 0 THEN
    PRINT #2, Margin$ + TabLevel$ + Text$
  ELSE
    IF Text$ = "" THEN Text$ = " " ' allows printing of blank lines
    DO UNTIL Text$ = ""
      Text$ = Margin$ + TabLevel$ + CmntSplit$ + Text$
      split% = LEN(Text$) + 1
      IF split% >= Length% THEN
        FOR split% = Length% TO 1 STEP -1
          IF MID$(Text$, split%, 1) = " " THEN EXIT FOR
        NEXT split%
      END IF
      IF split% < MinLen% THEN
        split% = INSTR(split% + 1, Text$, " ")
        IF split% = 0 THEN split% = LEN(Text$) + 1
      END IF
      PRINT #2, LEFT$(Text$, split% - 1)
      Text$ = MID$(Text$, split% + 1)
      CmntSplit$ = CmntKey$
    LOOP
  END IF
END SUB

SUB PrintProgramLine
  IF Comment$ <> "" AND CmtCol% >= 0 THEN Comment$ = " " + Comment$

  SELECT CASE CmtCol%
    CASE IS >= 0
      sLen% = LEN(Statement$)
      IF sLen% < CmtCol% THEN Statement$ = Statement$ + SPACE$(CmtCol% - sLen%)
      PrintLine (Statement$ + Comment$)
    CASE CmtABOVE%
      IF NOT CmtFlag% AND Comment$ <> "" THEN PrintLine (Comment$)
      PrintLine (Statement$)
    CASE CmtBELOW%
      PrintLine (Statement$)
      IF NOT CmtFlag% AND Comment$ <> "" THEN PrintLine (Comment$)
  END SELECT
END SUB

SUB SetReserved
  RESTORE ReservedData

  FOR Reserved% = ASC("A") TO ASC("Z")
    READ Reserved$(Reserved%)
  NEXT
END SUB

SUB SetStructure (Max%)
  RESTORE StructureData

  FOR Mdx% = 0 TO Max%
    READ Kwd$, Routine$
    Structure$(Mdx%, 1) = Kwd$
    Structure$(Mdx%, 2) = Routine$
  NEXT Mdx%
END SUB

FUNCTION StructureProgramLine%
  StructureProgramLine% = -1 ' preset for no keyword found
  IF NOT CmtFlag% THEN ' if it's not a full line comment
    IF LetFlag% THEN Statement$ = DeLETify$(Statement$) ' suppress useless "LET"statements

    ' DEF FNx does not follow the rules, so...
    IF INSTR(" " + UCASE$(Statement$), " " + "DEF FN") THEN
      Structure% = 26
    ELSE
      FOR Structure% = 1 TO MaxStruct% ' check for structuring stmts
        IF INSTR(" " + Statement$ + " ", " " + Structure$(Structure%, 1) + " ") THEN EXIT FOR
        IF INSTR(" " + Statement$ + ":", " " + Structure$(Structure%, 1) + ":") THEN EXIT FOR
      NEXT Structure%
    END IF
    IF Structure% > MaxStruct% THEN Structure% = 0

    SELECT CASE Structure$(Structure%, 2)
      CASE "NoS": EXIT FUNCTION
      CASE "unN": unNest
      CASE "doI": doIF
      CASE "doDO": doDO
      CASE "doC": doCASEELSE
      CASE "doFO": doFOR
      CASE "doN": doNEXT
      CASE "doFU": doFUNCSUB
      CASE "doS": doSELECT
      CASE "doE": doENDSEL
      CASE "doT": doTYPE
      CASE "doDEF": doDEFFN
      CASE "doW": doWHILE
      CASE "doDEC": doDECLARE
    END SELECT
  END IF

  StructureProgramLine% = PrintFlag%
END FUNCTION

FUNCTION TrimWhite$ (Text$)
  FOR tdx% = 1 TO LEN(Text$)
    IF MID$(Text$, tdx%, 1) > " " THEN EXIT FOR
  NEXT tdx%

  TrimWhite$ = MID$(Text$, tdx%)
END FUNCTION

SUB unNest
IF LEFT$(Statement$, 6) = "END IF" OR LEFT$(Statement$, 4) = "LOOP" THEN GOTO inside
IF LEFT$(Statement$, 4) = "NEXT" OR LEFT$(Statement$, 4) = "CASE" THEN GOTO inside
IF LEFT$(Statement$, 4) = "ELSE" OR LEFT$(Statement$, 12) = "END FUNCTION" THEN GOTO inside
IF LEFT$(Statement$, 7) = "END SUB" OR LEFT$(Statement$, 10) = "END SELECT" THEN GOTO inside
IF LEFT$(Statement$, 4) = "WEND" THEN GOTO inside
IF LEFT$(Statement$, 8) = "END TYPE" OR LEFT$(Statement$, 7) = "END DEF" THEN GOTO inside
GOTO outside
inside:
    IF TabLevel% THEN
      TabLevel% = TabLevel% - TabWidth%
      TabLevel$ = STRING$(TabLevel%, " ")
    END IF
outside:

END SUB

SUB Usage
  CLS
  PRINT "                         Quick Basic Beautifier"
  PRINT
  PRINT "Usage:"
  PRINT
  PRINT "  qbb InFile[.Ext] [OutFile[.Ext]] [/C:A|B|#] [/L] [/M:#[,#]] [/R:?] [/T:#] [/Q]"
  PRINT
  PRINT "  File extentions are optional.  If the InFile extension is omitted,"
  PRINT "  'BAS' is assumed.  If the OutFile extension is omitted, 'QBB' is"
  PRINT "  assumed.  If the OutFile is omitted, 'InFile.QBB' is assumed."
  PRINT "  If the OutFile starts with a '.', OutFile will end up being"
  PRINT "  'InFile.<specified ext>'."
  PRINT
  PRINT "  The colons on the switches are optional.  See the internal comments"
  PRINT "  for documentation on the switches."

  END
END SUB

