3

I have been experimenting with Python socket servers and such. I came across an idea and I am having a hard time implementing it. I want the server side to be able to enter different commands, for starting and stopping the server, and performing various other tasks. My problem is, when I start having a lot of commands, my program ends up looking like spaghetti:

if command == "start":
    print("Starting server")
    time.sleep(1)
    listener_thread.start()
elif command == "stop":
    print("Stopping server...")
    time.sleep(1)
    listener_thread.stop()
elif command in ["q", "quit"]:
    print("Quitting server...")
    time.sleep(1)
    for t in connections:
        t.stop()
    listener_thread.stop()
    exit()
else:
    print("Invalid command")

One of my friends who has been programming for a while said I should try and use a dictionary to store a function reference for each command. I created a dictionary like this:

commands = {
    "start": cmd_start, # This would be a reference to cmd_start()
    "stop": cmd_stop, # Same here and so forth
    "quit": cmd_quit
}

And I would call these commands like this:

while True:
    command = input("enter a command: ")
    if command in commands:
        commands[command]()

The problem with this method is that sometimes I want a command with multiple arguments and sometimes I don't. I want to be able to have different commands with varying arguments, specify their required arguments, and check to make sure the command is a valid command with all the required arguments. I am new to programming, and I have tried thinking of a clean way to implement this. I found nothing useful on google so hopefully someone can assist me. Thanks.

coder guy
  • 531
  • 1
  • 3
  • 12
  • I think your idea of a commands map is good, the thing you want to do is make the argument to each command a variable list of arguments and parse the user's input. Then call the command and pass it all the extra arguments the user typed. –  Oct 19 '15 at 22:29
  • Try reading about the *args and **kwargs parameters. – Prune Oct 19 '15 at 22:29
  • Lookup the first word of the input in your dict. Then use args, kwargs as per the 2 above suggestion and pass words[1:] to the command and let it figure out what to do. I'd draft in argparse module for commands with params. – JL Peyret Oct 19 '15 at 22:34
  • 1
    Look up the `click` library, or the `argparse` module. – tzaman Oct 19 '15 at 22:34
  • Here is what I came up with: http://pastebin.com/Kqc5gky4 – coder guy Oct 19 '15 at 22:46

2 Answers2

2

If you know the commands' structure this is a task of parsing and it is up to the format. Other than that you can send variable length arguments using the star operator * (you can also send keyword arguments using ** but I'd start with this).

Here is a simple example:

command = input("enter a command: ")
arguments = input("enter arguments separated by a single space: ").split()
if command in commands:
    commands[command](*arguments)

Note than this will send all arguments as a string. This is a basic demonstration:

>>> def func_with_three_params(a, b, c):
...     print a, b, c
... 
>>> three_args = "1 2 3".split()
>>> func_with_three_params(*three_args)
1 2 3

As mentioned in the comments to your question this is a very common task and libraries do exist to parse various common formats. One which is often used (I use it as well) is argparse.

Community
  • 1
  • 1
Reut Sharabani
  • 30,449
  • 6
  • 70
  • 88
  • I was under the impression that argparse was for passing in commands at runtime, and not while the script was running. Anyways, I tried what you and others said about reading up on *args and **kwargs. Here is what I came up with: http://pastebin.com/Kqc5gky4 - Does it look pythonic enough? Any recommendations? – coder guy Oct 19 '15 at 22:43
  • It's the common use, yeah - but there may be a way of hooking arguments into it dynamically. Probably using `parse_args`. About your code - try the code review site. – Reut Sharabani Oct 19 '15 at 22:50
1

A simple working example that is close to the poster's original source code:

tokens = input("$ ").split()

command, arguments = tokens[0], tokens[1:]

def start_handler(start_line, end_line):
    print("Starting from {} to {}".format(start_line, end_line))

commands = {
  "start": start_handler    
}

commands[command](*arguments)

You could enter a command like: start 1 20 and it would pass 1 and 20 to the start handler. Example output:

$  start 1 20
Starting from 1 to 20
  • I like the way you split the arguments and command up using a tuple. It makes the code more readable. I'll definitely tweak my program using that. – coder guy Oct 19 '15 at 22:46