pro mg_opt::setProperty, short_name=shortName
  compile_opt strictarr
  
  if (n_elements(shortName) gt 0L) then begin
    self.shortName = shortName
  endif
end
pro mg_opt::getProperty, long_name=longName, short_name=shortName, $
                         boolean=boolean, metavar=metavar, $
                         key_column_width=keyColumnWidth, $
                         help_header=helpHeader
  compile_opt strictarr
  
  if (arg_present(longName)) then longName = self.longName
  if (arg_present(shortName)) then shortName = self.shortName
  if (arg_present(boolean)) then boolean = self.boolean
  if (arg_present(metavar)) then metavar = self.metavar
  
  if (arg_present(keyColumnWidth) || arg_present(helpHeader)) then begin
    helpHeader = '--' + self.longName
    helpText = self.metavar eq '' ? strupcase(self.longName) : self.metavar
    
    if (~self.boolean) then begin
      helpHeader += '=' + helpText
    endif
    
    if (self.shortName ne '') then begin
      helpHeader += ', -' + self.shortName
      
      if (~self.boolean) then helpHeader += ' ' + helpText
    endif
    
    keyColumnWidth = strlen(helpHeader)
  endif
end
function mg_opt::isPresent
  compile_opt strictarr
  
  return, self.present
end
function mg_opt::getHelp
  compile_opt strictarr
  
  return, self.help
end
function mg_opt::getValue, present=present
  compile_opt strictarr
  present = self.present
  
  if (self.boolean) then begin
    return, self.present ? 1B : 0B
  endif else begin
    return, self.present ? self.value : self.default
  endelse
end
pro mg_opt::setValue, value
  compile_opt strictarr
  on_error, 2
  
  self.present = 1B
  if (n_elements(value) gt 0L) then begin
    self.value = value
  endif else begin
    if (~self.boolean) then begin
      message, 'non-boolean options like --' + self.longName + ' must have a value if present'
    endif
  endelse
end
function mg_opt::init, long_name=longName, boolean=boolean, $
                       help=help, default=default, metavar=metavar
  compile_opt strictarr
  
  self.longName = longName
  self.boolean = keyword_set(boolean)
  self.help = n_elements(help) gt 0L ? help : ''
  self.default = n_elements(default) gt 0L ? default : ''
  self.metavar = n_elements(metavar) gt 0L ? metavar : ''
  
  return, 1
end
pro mg_opt__define
  compile_opt strictarr
  
  define = { mg_opt, $
             longName: '', $
             shortName: '', $
             value: '', $
             present: 0B, $
             boolean: 0B, $
             metavar: '', $
             help: '', $
             default: '' }
end
function mg_options::get, optname, params=params, n_params=nparams, $
                          present=present
  compile_opt strictarr
  on_error, 2
  
  if (keyword_set(params)) then return, self.params->get(/all, count=nparams)
  
  opt = self.longOptions->get(optname, found=found)
  if (~found) then message, string(optname, format='(%"option %s not found")')
  return, opt->getValue(present=present)
end
pro mg_options::_displayHelp
  compile_opt strictarr
  keys = self.longOptions->keys(count=nkeys)
  shortNames = self.shortOptions->keys(count=nshortNames)
  
  args = ''
  if (self.nparamsAccepted[0] gt 0L) then begin
    args += strjoin('arg' + strtrim(indgen(self.nparamsAccepted[0]) + 1, 2), ' ')
  endif
  
  case 1 of
    self.nParamsAccepted[1] lt 0L: args += '...'
    self.nParamsAccepted[1] eq 0L:
    self.nParamsAccepted[1] gt 0L: begin
        if (self.nparamsAccepted[1] gt self.nparamsAccepted[0]) then begin
          optionalArgIndices $
            = indgen(self.nparamsAccepted[1] - self.nparamsAccepted[0]) $
                + self.nparamsAccepted[0]
          if (args ne '') then args += ' '
          args += strjoin('[arg' + strtrim(optionalArgIndices, 2) + ']', ' ')      
        endif        
      end
  endcase 
  
  print, self.appname, args, format='(%"usage: %s [options] %s")'
  print 
  
  keyColumnWidth = 2L
  maxKeyColumnWidth = 24L
  for k = 0L, nkeys - 1L do begin
    opt = self.longOptions->get(keys[k])
    opt->getProperty, key_column_width=keyWidth
    keyColumnWidth >= keyWidth
  endfor
  
  keyColumnWidth = keyColumnWidth <
  combinedFormat = '(%"  %-' + strtrim(keyColumnWidth, 2L) + 's  %s")'
  splitFormat1 = '(%"  %-0s")'
  splitFormat2 = '(%"' + strjoin((strarr(maxKeyColumnWidth + 4L) + ' ')) + '%s")'
  
  print, 'options:'
  keyind = sort(keys)  
  for k = 0L, nkeys - 1L do begin
    opt = self.longOptions->get(keys[keyind[k]])
    opt->getProperty, help_header=helpHeader
    if (strlen(helpHeader) gt maxKeyColumnWidth) then begin
      print, helpHeader, format=splitFormat1
      print, opt->getHelp(), format=splitFormat2
    endif else begin
      print, helpHeader, opt->getHelp(), format=combinedFormat
    endelse
  endfor
end
pro mg_options::_displayVersion
  compile_opt strictarr
  print, self.appname, self.version, format='(%"%s %s")'
end
pro mg_options::parseArgs, args, error_message=errorMsg
  compile_opt strictarr
  
  errorMsg = ''
  
  _args = n_elements(args) eq 0L ? command_line_args(count=nargs) : args
  _nargs = n_elements(nargs) eq 0L ? n_elements(args) : nargs
  
  argumentExpected = 0B
  
  for a = 0L, _nargs - 1L do begin
    
    if (argumentExpected) then begin
      opt->setValue, _args[a]
      argumentExpected = 0B
      continue
    endif
    
    
    if (strpos(_args[a], '--') eq 0L) then begin
      equalpos = strpos(_args[a], '=')
      if (equalpos eq -1L) then begin
        optname = strmid(_args[a], 2L)
      endif else begin
        optname = strmid(_args[a], 2L, equalpos - 2L)
      endelse
      
      opt = self.longOptions->get(optname, found=found)
      opt->getProperty, boolean=boolean
      
      if (~found) then begin
        errorMsg = string(optname, format='(%"unknown option: --%s")')
        return
      endif
      
      if (boolean) then begin
        opt->setValue
      endif else begin
        if (equalpos eq -1L) then begin
          argumentExpected = 1B
        endif else begin
          opt->setValue, strmid(_args[a], equalpos + 1L)
        endelse
      endelse
      
      continue
    endif
    
    
    if (strpos(_args[a], '-') eq 0L) then begin
      for sf = 1L, strlen(_args[a]) - 1L do begin
        shortName = strmid(_args[a], sf, 1)
        opt = self.shortOptions->get(shortName, found=found)
        
        if (~found) then begin
          errorMsg = string(shortName, format='(%"unknown option: -%s")')
          return
        endif
        opt->getProperty, boolean=boolean
        if (boolean) then begin
          opt->setValue
        endif else begin
          
          if (sf ne strlen(_args[a]) - 1L) then begin
            msg = '(%"non-boolean option -%s must be specified last to accept a value")'
            errorMsg = string(shortName, format=msg)
            return
          endif
          argumentExpected = 1B
        endelse
      endfor
      
      continue
    endif
    
    
    self.params->add, _args[a]
  endfor
  if (argumentExpected) then begin
    errorMsg = string(_args[a - 1L], format='(%"argument expected for %s")')
    return
  endif
  helpOpt = self.longOptions->get('help')
  if (helpOpt->isPresent()) then self->_displayHelp
  
  versionOpt = self.longOptions->get('version', found=versionFound)
  if (versionFound && versionOpt->isPresent()) then self->_displayVersion  
  
  if (helpOpt->isPresent() || (versionFound && versionOpt->isPresent())) then return
  
  nparams = self.params->count()
  if (nparams lt self.nParamsAccepted[0]) then begin
    errorMsg = string(self.nParamsAccepted[0], nparams, $
                      format='(%"%d parameters required, %d given")')
    return
  endif
  
  if (self.nParamsAccepted[1] ge 0L && nparams gt self.nParamsAccepted[1]) then begin
    errorMsg = string(self.nParamsAccepted[1], nparams, $
                      format='(%"%d parameters allowed, %d given")')
    return
  endif
end
pro mg_options::addOption, longForm, shortForm, help=help, default=default, $
                           boolean=boolean, metavar=metavar
  compile_opt strictarr
  
  opt = obj_new('mg_opt', $
                long_name=longForm, $
                boolean=boolean, help=help, default=default, metavar=metavar)
  self.longOptions->put, longForm, opt
  if (n_elements(shortForm) gt 0L) then begin
    self.shortOptions->put, shortForm, opt
    opt->setProperty, short_name=shortForm
  endif
end
pro mg_options::addParams, nparamsRange
  compile_opt strictarr
  on_error, 2
  
  if (nparamsRange[0] lt 0L) then begin
    message, 'minimum number of params must be positive'
  endif
  
  if (nparamsRange[1] gt 0L && (nparamsRange[1] lt nparamsRange[0])) then begin
    message, 'maximum number of params must be greater than minimum number'
  endif
  
  self.nParamsAccepted = nparamsRange
end
pro mg_options::cleanup
  compile_opt strictarr
  
  opts = self.longOptions->values(count=count)
  if (count gt 0L) then obj_destroy, opts
  
  
  obj_destroy, [self.longOptions, self.shortOptions, self.params]
end
function mg_options::init, app_name=appname, version=version
  compile_opt strictarr
  
  self.appname = n_elements(appname) eq 0L ? 'app' : appname
  self.version = n_elements(version) eq 0L ? '' : version
  
  self.longOptions = obj_new('MGcoHashTable', key_type=7, value_type=11)
  self.shortOptions = obj_new('MGcoHashTable', key_type=7, value_type=11)
  self.params = obj_new('MGcoArrayList', type=7)
  
  self->addOption, 'help', 'h', /boolean, help='display this help'
  if (self.version ne '') then begin
    self->addOption, 'version', /boolean, help='display version information'
  endif
  
  return, 1
end
pro mg_options__define
  compile_opt strictarr
  
  define = { mg_options, $
             appname: '', $
             version: '', $
             longOptions: obj_new(), $
             shortOptions: obj_new(), $
             params: obj_new(), $
             nParamsAccepted: lonarr(2) $
           }
end
opts = obj_new('mg_options', app_name='mg_options_example', version='1.0')
opts->addOption, 'verbose', 'v', $
                 /boolean, $
                 help='set to print a verbose greeting'
opts->addOption, 'name', 'n', help='name of user to greet', default='Mike', $
                 metavar='user''s name'
opts->parseArgs, error_message=errorMsg
if (errorMsg ne '') then begin
  oldQuiet = !quiet
  !quiet = 0
  message, errorMsg, /informational, /noname
  !quiet = oldQuiet
end
if (errorMsg eq '' && ~opts->get('help') && ~opts->get('version')) then begin
  print, (opts->get('verbose') ? 'Greetings and salutations' : 'Hello'), $
         opts->get('name'), $
         format='(%"%s, %s!")'
endif
obj_destroy, opts
end