[Module] General Purpose Console Interface

Share your advanced PureBasic knowledge/code with the community.
IndigoFuzz

[Module] General Purpose Console Interface

Post by IndigoFuzz »

A library I created for quickly creating console interfaces, though can also be used as a game console, or any other use cases where parsing commands and arguments from a string is needed.

Feedback welcome!

Commands
This library is designed to accept inputs in the form of "command arg1 arg2 arg3". If you need to use spaces in an argument, then encapsulate in double quotes.

Variables
In addition to fixed argument inputs, you can define variables which can be used in place of command/arguments by prefixing with '$'. I.e. $myvar
Setting a variable can be done using the inbuilt command 'setvar'.

Example

Code: Select all

setvar MyVar "Hello World"
print $MyVar
Built-in Help
Enable the built-in help by calling GCon::EnableHelp(). This will provide a nicely formatted table of commands, argument hints and descriptions.

Output
You can specify your own print output handler (shown in example) callback which directs information or error messages wherever you want (Useful if you have your own in-game console as an example)

Built-in Commands
You can disable these by using GCon::UnregisterCommand()

Code: Select all

setvar [name] [value] - Defined/set a variable value.
rmvar [name] - Remove a variable.
print [msg].. - Print each argument in a new line.
help - Display help information.  Can be enabled/disabled.  Disabled by default.

Code: Select all

; ====================================================================================================
; Title:  General Purpose Console-like Interface
; Author: IndigoFuzz
; Revision: 1
; ====================================================================================================

DeclareModule GCon
  
  Enumeration MessageType
    #Message_Info
    #Message_Error
  EndEnumeration
  
  Structure sCallInfo
    command.s
    argCount.i
    List args.s()
  EndStructure
  
  Prototype pCommandHandler(*CallInfo)
  Prototype pMessageHandler(MessageType, Message.s)
  
  ; Configuration
  Declare EnableHelp(Enable = #True)
  
  ; For printing to the defined message handler
  Declare PrintInfo(Info.s)
  Declare PrintError(Error.s)
  
  ; Fetch arguments
  Declare.s AsString(*CallInfo.sCallInfo, Index)
  Declare.i AsInteger(*CallInfo.sCallInfo, Index)
  Declare.a AsBool(*CallInfo.sCallInfo, Index)
  
  ; Command Methods
  Declare CommandNameValid(Name.s)
  Declare RegisterCommand(Command.s, Arguments.s, Description.s, *Handler.pCommandHandler)
  Declare UnregisterCommand(Command.s)

  ; Input Methods
  Declare ParseProgramParams()
  Declare ParseString(Input.s)
  
  ; Variable Methods
  Declare SetVar(VarName.s, Value.s)
  Declare.s GetVar(VarName.s, DefaultValue.s = "")
  Declare VarExists(VarName.s)
  Declare RemoveVar(VarName.s)
  
  ; Defines a message handler
  Declare SetMessageCallback(*MessageHandler.pMessageHandler)
  
EndDeclareModule

Module GCon
  
  Structure sCommandInfo
    command.s
    description.s
    arg.s
    *handler.pCommandHandler
  EndStructure
  
  Structure sGCon
    Map m_command.sCommandInfo()
    Map m_var.s()
    *m_messageHandler.pMessageHandler
    m_regEx.i
    m_enableHelp.a
  EndStructure
  
  Global gGCon.sGCon
  
  gGCon\m_regEx = CreateRegularExpression(#PB_Any, ~"(?:(['\"" + "])(.*?)(?<!\\)(?>\\\\)*\1|([^\s]+))")
  
  Procedure _InbuiltHelp(*CallInfo)
    Protected NewList cmd.s()
    Protected maxCmdLen.i, maxArgLen.i
    With gGCon
      ForEach \m_command()
        AddElement(cmd()) : cmd() = \m_command()\command
        If maxCmdLen < Len(cmd())
          maxCmdLen = Len(cmd())
        EndIf
        If maxArgLen < Len(\m_command()\arg)
          maxArgLen = Len(\m_command()\arg)
        EndIf
      Next
      SortList(cmd(), #PB_Sort_Ascending)
      PrintInfo("")
      PrintInfo("Available Commands:")
      PrintInfo("")
      ForEach cmd()
        If \m_command(cmd())\description <> ""
          PrintInfo(" " + RSet(cmd(), maxCmdLen + 2) + "  " + LSet(\m_command(cmd())\arg, maxargLen + 1) + " - " + \m_command(cmd())\description)
        Else
          PrintInfo(" " + cmd())
        EndIf
      Next
      PrintInfo("")
    EndWith
    FreeList(cmd())
  EndProcedure
  
  ; Built In Commands
  Procedure _OnSetVar(*Info.GCon::sCallInfo)
    If *Info\argCount = 2
      Protected.s varName = LCase(AsString(*Info, 0))
      Protected.s varValue = AsString(*Info, 1)
      If SetVar(varName, varValue) = #False
        PrintError("Could not set variable. Invalid variable name '" + varName + "'.")
      EndIf        
    Else
      PrintError(*Info\command + " expects 2 arguments.")
    EndIf    
  EndProcedure
  
  Procedure _OnRmVar(*Info.GCon::sCallInfo)
    ForEach *Info\args()
      RemoveVar(*Info\args())
    Next
  EndProcedure
  
  Procedure _OnPrint(*Info.GCon::sCallInfo)
    ForEach *Info\args()
      PrintInfo(*Info\args())
    Next
  EndProcedure
  
  RegisterCommand("setvar", "name, value", "Define/Set a variable.", @_OnSetVar())
  RegisterCommand("rmvar", "name", "Remove a variable.", @_OnSetVar())
  RegisterCommand("print", "message","Print a message.", @_OnPrint())
  
  Procedure _HandleVarTokens(List Tokens.s())
    Protected.s varname
    ForEach Tokens()
      ForEach gGCon\m_var()
        If FindString(Tokens(), "$" + MapKey(gGCon\m_var()), 1, #PB_String_NoCase)
          Tokens() = ReplaceString(Tokens(), "$" + MapKey(gGCon\m_var()), gGcon\m_var(), #PB_String_NoCase)
        EndIf
      Next
    Next
    ProcedureReturn #True
  EndProcedure
  
  Procedure _ParseTokens(List Tokens.s())
    Protected callInfo.sCallInfo, success.a
    If ListSize(Tokens()) > 0
      If _HandleVarTokens(Tokens()) = #False
        ProcedureReturn #False
      EndIf
      FirstElement(Tokens())
      Protected.s cmd = LCase(Tokens())
      If gGCon\m_enableHelp
        Select cmd
          Case "help", "?", "-h", "--help"
            _InbuiltHelp(0)
            ProcedureReturn #True
        EndSelect
      EndIf
      DeleteElement(Tokens())
      With callInfo
        ForEach Tokens()
          AddElement(\args()) : \args() = Tokens()
        Next
        \argCount = ListSize(Tokens())
      EndWith
      With gGCon
        If FindMapElement(\m_command(), cmd)
          Protected *handler.pCommandHandler = \m_command(cmd)\handler
          If *handler
            callInfo\command = cmd
            *handler(@callInfo)
            success = #True
          EndIf
        EndIf
      EndWith
      If success = 0
        If gGCon\m_enableHelp
          PrintError("Command '" + cmd + "' is not recognised. Use 'help' for assistance.")
        Else
          PrintError("Command '" + cmd + "' is not recognised.")
        EndIf
        ProcedureReturn #False
      EndIf
    Else
      ProcedureReturn #False
    EndIf
    ProcedureReturn #True
  EndProcedure
  
  Procedure EnableHelp(Enable = #True)
    If Enable
      gGCon\m_enableHelp = #True
    Else
      gGCon\m_enableHelp = #False
    EndIf
  EndProcedure
  
  Procedure PrintInfo(Info.s)
    If gGCon\m_messageHandler <> #Null
      gGCon\m_messageHandler(#Message_Info, Info)
    EndIf
  EndProcedure
  
  Procedure PrintError(Error.s)
    If gGCon\m_messageHandler <> #Null
      gGCon\m_messageHandler(#Message_Error, Error)
      gGCon\m_messageHandler(#Message_Error, "")
    EndIf
  EndProcedure
  
  Procedure.s AsString(*CallInfo.sCallInfo, Index)
    If *CallInfo And Index > -1
      With *CallInfo
        If \argCount > Index
          SelectElement(\args(), Index)
          ProcedureReturn \args()
        EndIf
      EndWith
    EndIf
    ProcedureReturn ""
  EndProcedure
  
  Procedure.i AsInteger(*CallInfo.sCallInfo, Index)
    If *CallInfo And Index > -1
      With *CallInfo
        If \argCount > Index
          SelectElement(\args(), Index)
          ProcedureReturn Val(\args())
        EndIf
      EndWith
    EndIf
    ProcedureReturn 0
  EndProcedure
  
  Procedure.a AsBool(*CallInfo.sCallInfo, Index)
    If *CallInfo And Index > -1
      With *CallInfo
        If \argCount > Index
          SelectElement(\args(), Index)
          Select LCase(\args())
            Case "true", "yes", "1", "on"
              ProcedureReturn #True
          EndSelect
        EndIf
      EndWith
    EndIf
    ProcedureReturn #False
  EndProcedure
  
  Procedure CommandNameValid(Name.s)
    Protected.s validChars = "abcdefghijklmnopqrstuvwxyz0123456789_-."
    Protected thisChar.s, ix.i
    Name = LCase(Name)
    If Len(Name) > 0
      For ix = 1 To Len(Name)
        If FindString(validChars, Mid(Name, ix, 1)) = 0
          ProcedureReturn #False
        EndIf
      Next
      ProcedureReturn #True
    Else
      ProcedureReturn #False
    EndIf
  EndProcedure
  
  Procedure RegisterCommand(Command.s, Arguments.s, Description.s, *Handler.pCommandHandler)
    If CommandNameValid(Command) And *Handler <> #Null
      Command = LCase(Command)
      With gGCon
        \m_command(Command)\command = Command
        \m_command(Command)\arg = Arguments
        \m_command(Command)\description = Description
        \m_command(Command)\handler = *Handler
        ProcedureReturn #True
      EndWith
    EndIf
    ProcedureReturn #False
  EndProcedure
  
  Procedure UnregisterCommand(Command.s)
    Command = LCase(Command)
    If DeleteMapElement(gGCon\m_command(), Command)
      ProcedureReturn #True
    EndIf
    ProcedureReturn #False
  EndProcedure
  
  Procedure SetVar(VarName.s, Value.s)
    VarName = LCase(VarName)
    If CommandNameValid(VarName)
      gGCon\m_var(VarName) = Value
      ProcedureReturn #True
    EndIf
    ProcedureReturn #False
  EndProcedure    
  
  Procedure.s GetVar(VarName.s, DefaultValue.s = "")
    VarName = LCase(VarName)
    If FindMapElement(gGCon\m_var(), VarName)
      ProcedureReturn gGCon\m_var(VarName)
    EndIf
    ProcedureReturn DefaultValue
  EndProcedure
  
  Procedure VarExists(VarName.s)
    VarName = LCase(VarName)
    If FindMapElement(gGCon\m_var(), VarName)
      ProcedureReturn #True
    EndIf
    ProcedureReturn #False
  EndProcedure
  
  Procedure RemoveVar(VarName.s)
    VarName = LCase(VarName)
    DeleteMapElement(gGCon\m_var(), VarName)
  EndProcedure
  
  Procedure ParseProgramParams()
    Protected ix.i
    Protected NewList Tok.s()
    For ix = 0 To CountProgramParameters() - 1
      AddElement(Tok()) : Tok() = ProgramParameter(ix)
    Next
    If ListSize(Tok()) > 0
      ProcedureReturn _ParseTokens(Tok())
    Else
      ProcedureReturn #True
    EndIf
  EndProcedure
  
  Procedure ParseString(Input.s)
    Protected NewList Tok.s()
    Input = Trim(Input)
    ExamineRegularExpression(gGCon\m_regEx, Input)
    While NextRegularExpressionMatch(gGCon\m_regEx)
      AddElement(Tok()) : Tok() = ReplaceString(RegularExpressionMatchString(gGCon\m_regEx), Chr(34), "")
    Wend
    If ListSize(Tok()) > 0
      ProcedureReturn _ParseTokens(Tok())
    Else
      ProcedureReturn #True
    EndIf
  EndProcedure
  
  Procedure SetMessageCallback(*MessageHandler.pMessageHandler)
    With gGCon
      \m_messageHandler = *MessageHandler
    EndWith
  EndProcedure
  
EndModule


;-
;- ====== SAMPLE ======
;- 
CompilerIf #PB_Compiler_IsMainFile
  
  ; Open Console
  OpenConsole("Interactive GCon")
  EnableGraphicalConsole(#True)
  
  ; GCon Output Handler
  Procedure MessageCallback(MessageType, Message.s)
    Select MessageType
      Case GCon::#Message_Info
        ConsoleColor(7, 0)
      Case GCon::#Message_Error
        ConsoleColor(12, 0)  
    EndSelect
    PrintN(Message)
    ConsoleColor(7, 0) ; Revert to Default
  EndProcedure
  
  GCon::SetMessageCallback(@MessageCallback())
  
  ; Message Box Command
  Procedure MyMessageBox(*Info.GCon::sCallInfo)
    Protected.s caption, body
    caption = "Information"
    Select *Info\argCount
      Case 1
        body = GCon::AsString(*Info, 0)
      Case 2
        body = GCon::AsString(*Info, 0)
        caption = GCon::AsString(*Info, 1)
      Default
        GCon::PrintError("'msgbox' expects 1-2 arguments.")
        ProcedureReturn #False
    EndSelect
    MessageRequester(caption, body)
  EndProcedure
  
  GCon::RegisterCommand("msgbox", "body [,caption]", "Open a Message Box.", @MyMessageBox())
  
  ; Run File Command
  Procedure MyRun(*Info.GCon::sCallInfo)
    Protected.s file
    Select *Info\argCount
      Case 1
        file = GCon::AsString(*Info, 0)
        If FileSize(file) > -1
          Define.i hFile = ReadFile(#PB_Any, file), lineIx.i
          Define.s line
          While Eof(hFile) = #False
            lineIx + 1
            line = ReadString(hFile)
            If GCon::ParseString(line) = #False
              GCon::PrintError("Error on line " + Str(lineIx) + ". (" + line + ")")
              Break
            EndIf
          Wend
          CloseFile(hFile)
        Else
          GCon::PrintError("File '" + file  + "' does not exist.")
        EndIf
      Default
        GCon::PrintError("'run' expects 1 argument.")
        ProcedureReturn #False
    EndSelect
  EndProcedure
  
  GCon::RegisterCommand("run", "file", "Run a file containing commands.", @MyRun())
  
  ; Enable Help Feature
  GCon::EnableHelp()
  
  ; Enter Interactive Loop
  Define.s input
  
  PrintN("Interactive console example.  Use 'help' for a list of commands, or 'end' to terminate the program.")
  PrintN("")
  
  Repeat
    
    Print("> ")
    input = Input()  
    If LCase(input) = "end"
      Break
    EndIf  
    
    GCon::ParseString(input)
  ForEver
  
  End 0
CompilerEndIf
User avatar
Kwai chang caine
Always Here
Always Here
Posts: 5342
Joined: Sun Nov 05, 2006 11:42 pm
Location: Lyon - France

Re: [Module] General Purpose Console Interface

Post by Kwai chang caine »

Thanks for sharing 8)
I was wondering, what is the difference between your and windows console ? :oops:
ImageThe happiness is a road...
Not a destination
IndigoFuzz

Re: [Module] General Purpose Console Interface

Post by IndigoFuzz »

Kwai chang caine wrote:Thanks for sharing 8)
I was wondering, what is the difference between your and windows console ? :oops:
It's merely a building block for quick creation of console applications where the programmer just wants a swift way to create action/command orientated apps.

For instance; If you wanted to create a console app which could do multiple things, rather than having to write a bunch of select statements, manually create a help command, you could just include this, and define the actions/commands as procedures, and fetch the arguments with module methods quickly, and (by enabling the help action) have it deliver a nicely formatted help display with your commands, hints on what arguments are needed.

Another use case could be if someone is writing a game where they wanted an in-game console for things like cheats, or developer commands. Just include the module, and it just saves a bit of time and allows for quick creation of actions.

I created it after it dawned on me I was constantly having to write the select/case logic, manually creating help providers, and it got finicky as the applications grew, so wrote this as a drop-in solution.
User avatar
Kwai chang caine
Always Here
Always Here
Posts: 5342
Joined: Sun Nov 05, 2006 11:42 pm
Location: Lyon - France

Re: [Module] General Purpose Console Interface

Post by Kwai chang caine »

Aaah ok :D
Thanks for your great explanation 8)
ImageThe happiness is a road...
Not a destination
Post Reply