Good evening

I have a certain system of various components (basically, just Python processes) running on my server. Each of these units has an open socket on a defined port, which I can connect to via telnet or netcat to open a command console specific to this component of my system.

In this article, I will give some tips on how to optimize and ease the process of connection and operation of such consoles.

The basic command

I use Linux, and I prefer netcat over telnet, personally. This is the most basic command I can use to connect to a command console

nc -v myserv.totallynottheexpert.net 12345

The -v flag makes netcat a bit more verbose: for example, it tells me that the connection was successful or that it was refused (because the component is down or the command-receiving socket is dead); without this flag it just connects or fails to connect silently. It’s not mandatory at all, I just like to have it for better control.

So, I get connected, and I type in some commands. After some time I want to use the same commands without retyping them. I press the up arrow, and… well…

Input history. Not enabled by default?

Ummm, no. Bash stores the history of commands, but my command consoles don’t. So, whenever you press the up key, it shows gibberish instead (something like ^[[A. This is probably some kind of a key code corresponding to the up arrow which is sent to the shell and then deciphered by it, but not sure). Implementing history in my programs might be problematic (okay, I just don’t know how to do it yet). I might want to use something system-wide.

There is a utility called rlwrap. It essentially adds the input history to our console; all you need to do is prepend it:

rlwrap nc -v myserv.totallynottheexpert.net 12345

There is one minor issue: if you open one console this way, issue some commands, then open another, the history from the first console will be transferred to the second console. It’s not a big deal for me though. Moreover, it is good: command history persists between connections to the consoles, and I don’t have to type them again after a new login!

Port number? Ummmm, don’t remember…

The next problem is that I have over a dozen components running on my server, and I don’t remember the port number for each command console. These port numbers are written in the configuration file called configuration.py in a dictionary COMMAND_PORTS; here’s what it looks like approximately:

COMMAND_PORTS = {
    'stargazer': 12345,
    'big_boomer': 45811,
    'double_defender': 18566,
}

Maybe we could read it from there directly and, instead of typing in a port number, provide a component’s name instead? It can be done with some Bash magic. But first, let’s write a small script that simply prints a port number by name.

import os,sys
sys.path=['/path/to/my/project'] + sys.path
from config import COMMAND_PORTS as PO
print(PO['component_name'])

It’s simple. Now, let’s create a Bash function that will run this script. I’ll append the following to ~/.bashrc:

myservconn() { rlwrap nc -v myserv.totallynottheexpert.net $(python3 -c "import os,sys; sys.path=['/path/to/my/project'] + sys.path; from config import COMMAND_PORTS as PO; print(PO['$1'])") ; }

I have simply squashed the script above into one line (notice the semicolons) and provided it to python3 (notice the -c flag, it allows you to run Python scripts from the console immediately). The printed result (the port number) is provided to the command we are familiar with already. Also, notice the $1 in the Python script. Something not so pythonic, right? Yeah, it’s a Bash feature, it takes the first argument of the function (whatever it is. Basically, a nameless argument) and places it there. So, running this becomes as simple as:

myservconn stargazer

And it connects! Fantastic! The command is now so much simpler!

Component name? What was it again?

I have a baaaad memory, sometimes I forget the exact names of my components. Would be great if we had an autocomplete on this command that would show and fill component names when you press Tab. I have found a nice tip on how this is done. Here’s another thing I appended to ~/.bashrc:

_myservconn() {
    local cur=${COMP_WORDS[COMP_CWORD]}
    THE_KEYS=$( python3 -c "import os,sys; sys.path=['/path/to/my/project'] + sys.path; from config import COMMAND_PORTS as PO; print(' '.join(PO.keys()))" );
    COMPREPLY=( $(compgen -W "$THE_KEYS" -- $cur) )
}

complete -F _myservconn myservconn

Let’s explain what happens here. The first line in this function is a local variable cur (meaning that it exists only within the scope of this function). We assign something strange to it. From GNU docs:

COMP_WORDS

An array variable consisting of the individual words in the current command line. The line is split into words as Readline would split it, using COMP_WORDBREAKS as described above. This variable is available only in shell functions invoked by the programmable completion facilities.

COMP_CWORD

An index into ${COMP_WORDS} of the word containing the current cursor position. This variable is available only in shell functions invoked by the programmable completion facilities.

So it takes the array of everything that is currently typed into command line (split by spaces) and takes the word the cursor is currently at.

Next, we run a Python script as we did before. But we changed the last line so it would print all the keys in our dictionary as space-separated words.

Then, we provide the keys as options and current word as final argument to compgen. From docs:

compgen [option] [word]

Generate possible completion matches for word according to the options,[..]

It returns an array of words from the given space-separated list (-W option) that correspond to a given word. Finally, we assign this result to COMPREPLY, which is:

An array variable from which Bash reads the possible completions generated by a shell function invoked by the programmable completion facility (see Programmable Completion). Each array element contains one possible completion.

And finally, after the function we register it as an autocompletion handler (-F option) for our function using complete built-in.

Now, whenever I use the myservconn command, I can press Tab and get the list of all available component names. Perfect! No need to remember stuff now, I can be lazy, hahah.

In conclusion

This was simply a set of tips that might be helpful if you want to shorten a command, implement autocomplete or add a command history to a shell or console that does not support it.

As always, thanks for reading. I will see you eventually.