#22 Registering Hooks in Various Sections of Runtime



Been messing around with some of the internals of PyCmd again, and I ended yo setting up a way to register hooks at a few different parts of the runtime cycle from init.py. Right now, I have it set up so that the init.py can register a hook at initialization (though in hindsight, that's probably not really necessary since any necessary initialization can easily be handled in init.py itself), during the main loop (in run_cmd, to be exact), and at tab completion.

For instance, hooking into the main_loop:

def print_tokens(cmd, args):
print cmd, args
return (False, (cmd, args))

register_hook(MAIN_HOOK, print_tokens)

So with that in my init.py, running ls --color=always would look like this:

[PyCmd]$ ls --color=always
ls [u'--color=always']
ANSI32.dll init.py pycmd_public.py
custom_commands.py old.rar pycmd_public.pyc
custom_commands.pyo PyCmd.exe pycmd_public.pyo

A hook registered with the type MAIN_HOOK excepts a a return of a tuple containing either (handled, (cmd, args)) or (handled, None) with handled being whether or not the hook handled execution of the cmd. (if True, run_cmd would then return) (cmd, args) is only needed if the hook wants to modify the original cmd and args specified. If no modification is necessary, that can simple be None.

I did it so that I could implement linux-style aliases, so that I could do stuff like:

[PyCmd]$ alias -p

  1. gitcl = git clone

[PyCmd]$ gitcl git://pycmd.git.sourceforge.net/gitroot/pycmd/pycmd pycmd
Cloning into pycmd...
remote: Counting objects: 511, done.
remote: Compressing objects: 100% (507/507), done.
remote: Total 511 (delta 328), reused 0 (delta 0)Receiving objects: 36% (184/511)
Receiving objects: 100% (511/511), 143.97 KiB, done.
Resolving deltas: 100% (328/328), done.


Doesn't really conform to cmd.exe compatibility, so I understand if it wont work. Anyways, if you're interested, I'll need to remove a couple of other things I've been messing around with, and then I'll attach a patch here.


  • Horea Haitonic

    Horea Haitonic - 2012-01-24


    Aliases are something that PyCmd could definitely use; I'm not sure that hooks are the best mechanisms to implement them, though (hooks might still be useful for some other purposes as well).

    Please post a patch if it's not too much work for you, I would definitely like to see your implementation. It would be also nice to discuss the pros and cons of having aliases implemented with hooks instead of a more traditional approach like "aliases['gitcl'] = 'git clone' in init.py? Did you consider situations like "cd d:\pycmd && gitcl"?

  • juntalis

    juntalis - 2012-01-24

    Sure, I just need to pull another local copy of the repository so I can figure out what changes made should actually change. PyCmd has become my defacto sandbox for playing with new modules I find, so at the moment, the aliases are being loaded from a leveldb database, so let me go ahead and change it to use a flat file or something of the like.

    My whole idea for using hooks, rather than a simple find and replace was based more on the idea of custom commands that handle input completely. For instance, the alias command itself is a custom command that returns (True, None) to indicate that it handled the input given. So when run_cmd gets the True response, it simply returns and doesn't bother running any command. This could be useful for writing small commands that change PyCmd settings at runtime, such as tab completion behavior, and switching color schemes.

    My only concern with having an implementation that uses alias['gitcl'] = 'git clone' is commands like..

    alias['cpp'] = '"C:\Program Files\Microsoft Visual Studio 10.0\VC\bin\cl.exe" -O2 -EHsc -GL'

    Since the path to the program contains a space, we'd need to find a place to install the checks earlier in the process, or else PyCmd would end up trying to run ['C:\Program','Files\Microsoft', 'Visual', 'Studio','10.0\VC\bin\cl.exe', '-O2', '-EHsc', '-GL'].

    I didn't really consider compound commands in my thought process. I kind of assumed that they had been parsed into individual commands in the state machine loop, so unfortunately:

    [sandbox]$ alias -p
    alias gitcl='git clone'

    [sandbox]$ cd .. && gitcl help
    'gitcl' is not recognized as an internal or external command,
    operable program or batch file.

    I think I'll need to figure out another way to do this. Let me know what you think, and if you have any ideas, I'll implement them before I put together the patch.

  • juntalis

    juntalis - 2012-01-24

    Really wish SourceForge allowed you edit your comments. I just wanted to show you the actual command that handles the 'alias' input to give you an idea of what I mean by custom_cmds. You'll have to excuse the crappy, dirty options checking. I wrote it in a hurry to test the concept. I also just realized I was testing yserial when I wrote it, not leveldb.

        def get_alias(cmd):
            return pycmddb().select('alias:'+cmd, 'aliases')
        def alias_main(args):
            if len(args) == 0:
                args[0] = args[0].lower()
                if args[0] == '-?' or args[0] == '-h':
                elif args[0] == '-p':
                    if len(args) == 1:
                        aliases = pycmddb().selectdic('','aliases')
                        if len(aliases) == 0:
                            print 'No aliases currently defined.'
                            for i in aliases:
                                alias = aliases[i]
                                print u'alias %s=\'%s\'' % (alias[1].replace('alias:',''), u' '.join(alias[2]))
                    elif len(args) == 2:
                        alias = args[1]
                        aliasobj = pycmddb().select('alias:'+alias, 'aliases')
                        if aliasobj is None:
                            print 'Alias %s not found.' % alias
                            print u'Alias: %s = %s' % (alias, u' '.join(aliasobj))
                elif args[0] == '-u':
                    if len(args) == 2:
                        alias = args[1]
                        aliasobj = pycmddb().select('alias:'+alias, 'aliases')
                        if aliasobj is None:
                            print 'Alias %s not found.' % alias
                            pycmddb().delete('alias:'+alias, 'aliases')
                            print 'Alias %s deleted.' % alias
                    if len(args) == 1:
                        print 'Need a cmd to use.'
                        pycmddb().insert(args[1:], args[0], 'aliases')

    And the hook I registered to make this work was:

        def cmd_hook(cmd, args):
            cmd = cmd.lower()
            if cmd == u'alias':
                return (True, None)
            custom_cmd = get_custom_command(cmd)
            if custom_cmd is not None:
                result = custom_cmd(args)
                if result is None:
                if len(result) == 1:
                    return (False, (result[0], []))
                elif len(result) > 1:
                    return (False, (result[0], result[1:]))
                    return (True, None)
            alias = get_alias(cmd)
            if alias is not None:
                cmd = alias[0]
                if len(alias) > 1:
                    args = alias[1:]
                    args = []
                return (False, (cmd, args))
            return (False, None)
        register_hook(MAIN_HOOK, cmd_hook)

    I also wanted to mention some other changes I made that I'd actually like to see your opinion on. I exposed the following objects to init.py:

                         'pycmd_install_dir' : pycmd_install_dir,
                         'pycmd_data_dir' : pycmd_data_dir,
                         'register_hook' : register_hook,
                         'unregister_hook' : unregister_hook,
                         'INIT_HOOK' : hook_types[0],
                         'MAIN_HOOK' : hook_types[1],
                        'TAB_HOOK' : hook_types[2],

    The hook stuff is self-explanatory, but I thought that adding the install_dir and data_dir to the context of init.py would make it a bit easier for users to create configuration files/whatever in the appropriate folders without having to resolve the folder themselves.

  • juntalis

    juntalis - 2012-01-24

    Didn't realize Sourceforge would remove all indentation. Here's the code I have above as it should be.

  • juntalis

    juntalis - 2012-02-01

    Really wish Sourceforge allowed editing comments. Anyways, I had some time today, so I cleaned up the code, re-indented it, and threw together a patch, which I've attached to this post. Cheers.


Log in to post a comment.

Get latest updates about Open Source Projects, Conferences and News.

Sign up for the SourceForge newsletter:

No, thanks