GAM icon indicating copy to clipboard operation
GAM copied to clipboard

Argparse

Open alexwennerberg opened this issue 7 years ago • 15 comments

Have you thought about using the argparse library to parse user input rather than just iterating over sys.argv? I find it difficult to memorize GAM commands, which lack built-in documentation, don't distinguish between flags and input, and are inconsistent with other command line tools I'm familiar with. Obviously this would be a major change to the codebase, but I think it would substantially improve usability as well as make the code much easier to maintain.

Let me know what you think! Alex

alexwennerberg avatar Jan 19 '18 21:01 alexwennerberg

I was looking at it a bit.

I think one of the issues I see with implementing argparse in a nice way is actually structure of the commands.

They're not grouped by item we're taking action on, but by action itself, so it's: gam update (gam update user, gam update group) instead of gam update (gam group update, gam user update). Swapping the arguments would allow to have nice set of subparsers for each item you can take action on.

Not sure what's gam policy on changes breaking backwards compatibility.

pawelpbm avatar Mar 12 '19 13:03 pawelpbm

I have some proof of concept now:

pbm@tauri:~/GAM/src$ ./gam.py group
usage: gam.py group [-h] {info,delete} ...
gam.py group: error: too few arguments

pbm@tauri:~/GAM/src$ ./gam.py group info
usage: gam.py group info [-h] [--nousers] [--noaliases] [--groups] name
gam.py group info: error: too few arguments

Looks like moving to argparse would require quite a big changes in gam CLI interface. If I'm reading argparse docs correctly all flags would have to have "--" prefix. Also to make it nicer from code point of view and (I think) also from the user point of view, we could group functionality as I mentioned before - basically by swapping and :

gam info group -> gam group info
gam delete group -> gam group delete, etc

Also in few places I noticed inconsistent naming - for example there is gam calendar showacl to show ACLs, but function to add ACLs is just gam calendar add. That could be cleaned up as well.

@jay0lee: what's your opinion on that?

pawelpbm avatar Mar 13 '19 17:03 pawelpbm

I'm open to doing this but it's going to take some work. Rough thoughts:

  • I'm somewhat embarrassed by argument parsing in GAM today. sys.argv[i] all over everywhere is just ugly :-(
  • Pretty sure that in order to use argparse or something else, we'd need to break compatability with older GAM commands. I'm not opposed to using more standard -- style arguments though it will be a process for admins to pickup and learn the new commands.
  • Using argparse or similar should give us auto-generated help docs in some form or another which would be a huge win in terms of maintainng the wiki and answering command format questions on the discussion group.

My suggestion is to start small here. Start clean with a very small set of GAM APIs and commands, maybe something like users and groups. We can focus on a clean, simple and scalable format for these commands and then once its' agreed upon at that point, take on the work of moving the boatload of other APIs / commands into the new format.

jay0lee avatar Mar 14 '19 15:03 jay0lee

Pawel,

Send me a private email.

Ross

On Wed, Mar 13, 2019 at 10:16 AM Paweł Szubert [email protected] wrote:

I have some proof of concept now:

pbm@tauri:~/GAM/src$ ./gam.py group usage: gam.py group [-h] {info,delete} ... gam.py group: error: too few arguments

pbm@tauri:~/GAM/src$ ./gam.py group info usage: gam.py group info [-h] [--nousers] [--noaliases] [--groups] name gam.py group info: error: too few arguments

Looks like moving to argparse would require quite a big changes in gam CLI interface. If I'm reading argparse docs correctly all flags would have to have "--" prefix. Also to make it nicer from code point of view and (I think) also from the user point of view, we could group functionality as I mentioned before - basically by swapping and : gam info group -> gam group info gam delete group -> gam group delete, etc

Also in few places I noticed inconsistent naming - for example there is gam calendar showacl to show ACLs, but function to add ACLs is just gam calendar add. That could be cleaned up as well.

@jay0lee https://github.com/jay0lee: what's your opinion on that?

— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub https://github.com/jay0lee/GAM/issues/677#issuecomment-472520152, or mute the thread https://github.com/notifications/unsubscribe-auth/AIU8L8HnDvG_3svsMOTcDsyArP0hSNzZks5vWTJdgaJpZM4RlEwN .

-- Ross Scroggs [email protected]

taers232c avatar Mar 14 '19 15:03 taers232c

Ross, let's keep the discussion public please.

jay0lee avatar Mar 14 '19 15:03 jay0lee

Pawel,

I'm not sure why you would want to rewrite Jays' gam, I've already done that and you have a huge amount of work in front of you just to recode the parsing. You're left with a version lacking a significant number of features. Python 2: https://github.com/taers232c/GAMADV-XTD Python 3: https://github.com/taers232c/GAMADV-XTD3

Here's an example: Basic Gam:

gam info group <GroupItem> [nousers] [noaliases] [groups]

def doGetGroupInfo(group_name=None):
  cd = buildGAPIObject(u'directory')
  gs = buildGAPIObject(u'groupssettings')
  getAliases = getUsers = True
  getGroups = False
  if group_name is None:
    group_name = normalizeEmailAddressOrUID(sys.argv[3])
    i = 4
  else:
    i = 3
  while i < len(sys.argv):
    myarg = sys.argv[i].lower()
    if myarg == u'nousers':
      getUsers = False
      i += 1
    elif myarg == u'noaliases':
      getAliases = False
      i += 1
    elif myarg == u'groups':
      getGroups = True
      i += 1
    elif myarg in [u'nogroups', u'nolicenses', u'nolicences', u'noschemas', u'schemas', u'userview']:
      i += 1
      if myarg == u'schemas':
        i += 1
    else:
      systemErrorExit(2, '%s is not a valid argument for "gam info group"' % myarg)

Advanced GAM:

gam info group|groups <GroupEntity> [members] [managers] [owners] [nousers|notsuspended|suspended] [quick]
	[noaliases] [groups] <GroupFieldName>* [fields <GroupFieldNameList>] [formatjson]
                                                                               
def doInfoGroups():
  infoGroups(getEntityList(Cmd.OB_GROUP_ENTITY))

def infoGroups(entityList):
  cd = buildGAPIObject(API.DIRECTORY)
  getAliases = getUsers = True
  getGroups = getSettings = False
  FJQC = FormatJSONQuoteChar()
  groups = []
  members = []
  cdfieldsList = gsfieldsList = isSuspended = None
  entityType = Ent.MEMBER
  rolesSet = set()
  while Cmd.ArgumentsRemaining():
    myarg = getArgument()
    if myarg == u'quick':
      getAliases = getUsers = False
    elif myarg == u'nousers':
      getUsers = False
    elif myarg in SUSPENDED_ARGUMENTS:
      isSuspended = _getIsSuspended(myarg)
      entityType = [Ent.MEMBER_NOT_SUSPENDED, Ent.MEMBER_SUSPENDED][isSuspended]
    elif myarg == u'noaliases':
      getAliases = False
    elif myarg in GROUP_ROLES_MAP:
      rolesSet.add(GROUP_ROLES_MAP[myarg])
      getUsers = True
    elif myarg == u'groups':
      getGroups = True
    elif myarg in GROUP_FIELDS_CHOICE_MAP:
      if not cdfieldsList:
        cdfieldsList = [u'email',]
      addFieldToFieldsList(myarg, GROUP_FIELDS_CHOICE_MAP, cdfieldsList)
      if myarg in [u'name', u'description']:
        if not gsfieldsList:
          gsfieldsList = []
        gsfieldsList.append(myarg)
    elif myarg in GROUP_ATTRIBUTES:
      if not gsfieldsList:
        gsfieldsList = []
      gsfieldsList.extend([GROUP_ATTRIBUTES[myarg][0]])
    elif myarg == u'collaborative':
      if not gsfieldsList:
        gsfieldsList = []
      gsfieldsList.extend(COLLABORATIVE_ACL_ATTRIBUTES)
    elif myarg == u'fields':
      if not cdfieldsList:
        cdfieldsList = [u'email',]
      if not gsfieldsList:
        gsfieldsList = []
      for field in _getFieldsList():
        if field in GROUP_FIELDS_CHOICE_MAP:
          addFieldToFieldsList(field, GROUP_FIELDS_CHOICE_MAP, cdfieldsList)
          if field in [u'name', u'description']:
            gsfieldsList.append(field)
        elif field in GROUP_ATTRIBUTES:
          gsfieldsList.extend([GROUP_ATTRIBUTES[field][0]])
        elif field == u'collaborative':
          gsfieldsList.extend(COLLABORATIVE_ACL_ATTRIBUTES)
        else:
          invalidChoiceExit(list(GROUP_FIELDS_CHOICE_MAP)+list(GROUP_ATTRIBUTES), True)
# Ignore info user arguments that may have come from whatis                                                                                                                                                                                                               
    elif myarg in INFO_USER_OPTIONS:
      if myarg == u'schemas':
	getString(Cmd.OB_SCHEMA_NAME_LIST)
    else:
      FJQC.getFormatJSON(myarg)

sys.argv[i] all over everywhere is just ugly :-( All of the sys.argv processing is handled in functions

-- Ross Scroggs [email protected]

taers232c avatar Mar 14 '19 16:03 taers232c

Gam has a lot of inconsistency (gam info user ... vs gam user verb ...) and if the commandline syntax were reworked that'd be awesome, but it would need to be an optional method to prevent backward compatibility issues. For example 'gamc' as the program name to let it know the new syntax is in use, or use argument syntax instead of positional syntax, ala 'gam --user bri --info', so it could key off of the fact that there are double dash arguments to know it's the new syntax.

Anything else would cause chaos.

The implementation trick is that since everything currently is positionally based, the functions for processing the remaining sys.argv options are now embedded within a function. You'd essentially need to move all the actual API-calling code to their own functions, and have two entry points to them.

Doable, yes. Doable quickly, no. Which is why I never went down that road.

daethnir avatar Mar 14 '19 16:03 daethnir

After I wrote this issue I was motivated to implement it in a wrapper to Google APIs: https://github.com/Cloudbakers/agm

You all may find it interesting / useful or draw inspiration from it

alexwennerberg avatar Mar 14 '19 18:03 alexwennerberg

Again, it was a very early decision on my part with GAM to not use something like argparse so that GAM could have what I felt would be a more "natural" human readable language for commands. That decision has created a lot of inconsistency challenges over the years though.

I feel like if we're going to do this we should just go ahead and do it right and rely fully on something like argparse. No doubt it'll be a huge challenge for admins to move over to a rewritten GAM that uses argparse but the consistency and self-documenting capabilities will also make self-discovery a lot easier process for everyone.

jay0lee avatar Mar 14 '19 18:03 jay0lee

Also being open source and fairly self contained offers a decent workaround to "backward compatible". If you want backward compatability, use the old version :-) If you need new APIs and features you'll need to move to and learn the new version syntax.

jay0lee avatar Mar 14 '19 18:03 jay0lee

As an example, how would argparse handle this:

<UserAttributes> ::=
        (address clear|(type work|home|other|(custom <String>) [unstructured|formatted <String>] [pobox <String>] [extendedaddress <String>] [streetaddress <String>]
                [locality <String>] [region <String>] [postalcode <String>] [country <String>] [countrycode <String>] notprimary|primary))|
        (agreed2terms|agreedtoterms <Boolean>)|
        (changepassword|changepasswordatnextlogin <Boolean>)|
        (crypt|sha|sha1|sha-1|md5|nohash)|
        (customerid <String>)|
        (email|primaryemail|username <EmailAddress>)|
        (otheremail clear|(work|home|other|<String> <String>))|
        (externalid clear|(account|customer|login_id|network|organization|<String> <String>))|
        (firstname|givenname <String>)|
        (gal|includeinglobaladdresslist <Boolean>)|
        (gender clear|(female|male|unknown|(other <String>) [addressmeas <String>]))|
        (im clear|(type work|home|other|(custom <String>) protocol aim|gtalk|icq|jabber|msn|net_meeting|qq|skype|yahoo|(custom_protocol <String>) <String> [notprimary|primary]))|
        (ipwhitelisted <Boolean>)|
        (keyword clear|(occupation|outlook|(custom <string>) <String>))|
        (language clear|<LanguageList>)|
        (lastname|familyname <String>)|
        (location clear|(type default|desk|<String> area <String> [building|buildingid <BuildingID>] [floor|floorname <FloorName>] [section|floorsection <String>] [desk|deskcode <String>] endlocation))|
        (note clear|([text_plain|text_html] <String>|(file <FileName> [charset <Charset>])))|
        (organization clear|([type domain_only|school|unknown|work] [customtype <String>] [name <String>] [title <String>] [department <String>] [symbol <String>]
                [costcenter <String>]  [location <String>] [description <String>] [domain <String>] notprimary|primary))|
        (org|ou|orgunitpath <OrgUnitPath>)
        (password random|<Password>)|
        (phone clear|([type work|home|other|work_fax|home_fax|other_fax|main|company_main|assistant|mobile|work_mobile|pager|work_pager|car|radio|callback|isdn|telex|tty_tdd|grand_central|(custom <String>)]
                [value <String>] notprimary|primary))|
        (posix clear|(username <String> uid <Integer> gid <Integer> [system|systemid <String>] [home|homedirectory <String>] [shell <String>] [gecos <String>] [primary <Boolean>] endposix))|
        (relation clear|(spouse|child|mother|father|parent|brother|sister|friend|relative|domestic_partner|manager|dotted-line_manager|assistant|admin_assistant|exec_assistant|referred_by|partner|<String> <String>))|
        (sshkeys clear|(key <String> [expires <Integer>] endssh))|
        (suspended <Boolean>)|
        (website clear|(home_page|blog|profile|work|home|other|ftp|reservations|app_install_page|<String> <URL> [notprimary|primary]))|
        (<SchemaName>.<FieldName> [multivalued|multivalue|value|multinonempty [type work|home|other|(custom <String>)]] <String>)

gam update user <UserItem> <UserAttributes>* [clearschema <SchemaName>] [clearschema <SchemaName>.<FieldName>]

taers232c avatar Mar 14 '19 18:03 taers232c

Most objects would likely map to a argparse subcommand: https://docs.python.org/3/library/argparse.html#sub-commands

So something like: gam user --action create --email [email protected] --firstname Jay --lastname Lee --password S3cr3t

Another option would be to take a hint from Google Cloud's gcloud command which is pretty modular and use calliope: https://github.com/google-cloud-sdk/google-cloud-sdk https://github.com/edlane/cliapi

jay0lee avatar Mar 14 '19 18:03 jay0lee

There are two inter-mingled issues here:

  • How to parse the arguments - Changing this will be long and hard
  • A cleaner syntax - Not so difficult

Currently many non-user oriented commands look like this:

gam verb noun <Instance> <Options>

For example:

gam create group <EmailAddress> <GroupAttributes>*
gam update group <GroupItem> [admincreated <Boolean>] [email <EmailAddress>] <GroupAttributes>*
gam update group <GroupItem> add [owner|manager|member] [notsuspended|suspended] [allmail|daily|digest|none|nomail] <UserTypeEntity>
gam update group <GroupItem> delete|remove [owner|manager|member] <UserTypeEntity>
gam update group <GroupItem> sync [owner|manager|member] [notsuspended|suspended] [allmail|daily|digest|none|nomail] <UserTypeEntity>
gam update group <GroupItem> update [owner|manager|member] [notsuspended|suspended] [allmail|daily|digest|none|nomail] <UserTypeEntity>
gam update group <GroupItem> clear [member] [manager] [owner] [notsuspended|suspended]
gam delete group <GroupItem>
gam info group <GroupItem> [nousers] [noaliases] [groups]

Allowing this form as well would be simple and would present no backwards compatibility issues:

gam noun verb <Instance> <Options>

For example:

gam group create <EmailAddress> <GroupAttributes>*
gam group update <GroupItem> [admincreated <Boolean>] [email <EmailAddress>] <GroupAttributes>*
gam group update <GroupItem> add [owner|manager|member] [notsuspended|suspended] [allmail|daily|digest|none|nomail] <UserTypeEntity>
gam group update <GroupItem> delete|remove [owner|manager|member] <UserTypeEntity>
gam group update <GroupItem> sync [owner|manager|member] [notsuspended|suspended] [allmail|daily|digest|none|nomail] <UserTypeEntity>
gam group update <GroupItem> update [owner|manager|member] [notsuspended|suspended] [allmail|daily|digest|none|nomail] <UserTypeEntity>
gam group update <GroupItem> clear [member] [manager] [owner] [notsuspended|suspended]
gam group delete <GroupItem>
gam group info <GroupItem> [nousers] [noaliases] [groups]

The coding changes to do this are minimal.

I would focus first on cleaning up the syntax , Advanced GAM has done a lot of this already:

New syntax:

gam calendar <CalendarEntity> create|add acls <CalendarACLRole> <CalendarACLScopeEntity> [sendnotifications <Boolean>]
gam calendar <CalendarEntity> update acls <CalendarACLRole> <CalendarACLScopeEntity> [sendnotifications <Boolean>]
gam calendar <CalendarEntity> delete acls [<CalendarACLRole>] <CalendarACLScopeEntity>
gam calendar <CalendarEntity> info acls <CalendarACLScopeEntity> [formatjson]
gam calendar <CalendarEntity> show acls [formatjson]
gam calendar <CalendarEntity> print acls [todrive <ToDriveAttribute>*] [formatjson] [quotechar <Character>]

gam calendar <CalendarEntity> create|add event [id <String>] <EventCreateAttribute>+
gam calendar <CalendarEntity> import event icaluid <iCalUID> <EventImportAttribute>+
gam calendar <CalendarEntity> update events <EventEntity> <EventUpdateAttribute>+
gam calendar <CalendarEntity> delete events <EventEntity> [doit] [notifyattendees]
gam calendar <CalendarEntity> purge events <EventEntity> [doit] [notifyattendees]
gam calendar <CalendarEntity> wipe events
gam calendar <CalendarEntity> move events <EventEntity> to <CalendarItem> [notifyattendees]
gam calendar <CalendarEntity> empty calendartrash

Old syntax:

gam calendar <CalendarEntity> create|add <CalendarACLRole> ([user] <EmailAddress>)|(group <EmailAddress>)|(domain [<DomainName>])|default [sendnotifications <Boolean>]
gam calendar <CalendarEntity> update <CalendarACLRole> ([user] <EmailAddress>)|(group <EmailAddress>)|(domain [<DomainName>])|default [sendnotifications <Boolean>]
gam calendar <CalendarEntity> delete [<CalendarACLRole>] ([user] <EmailAddress>)|(group <EmailAddress>)|(domain [<DomainName>])|default
gam calendar <CalendarEntity> showacl [formatjson]

gam calendar <CalendarEntity> addevent <EventCreateAttribute>+
gam calendar <CalendarEntity> deleteevent <EventEntity> [doit] [notifyattendees]
gam calendar <CalendarEntity> wipe

The old syntax is supported for backwards compatibility.

taers232c avatar Mar 15 '19 16:03 taers232c

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs.

stale[bot] avatar Mar 05 '20 13:03 stale[bot]

Updating this issue to keep it active

ejochman avatar Mar 06 '20 04:03 ejochman