Sunday afternoon Python: Morse code

... That is to say the code was written on a Sunday afternoon. The publish date of this post may vary.

A lazy Sunday afternoon reading about Morse code made me hit upon the idea of a Python script that writes plain text into Morse code, and converts Morse code back to plain text. Some time spent mulling over the idea gave me a list of initial requirements: I would need to map plain text letters back to Morse code (and vice versa), let the user of the script choose between converting plain text into Morse code or vice versa, and take input from the user.

Mapping text to Morse code with Python Data Structures 

Mapping the plain text letters of the alphabet to Morse code can really be thought of as a pair like relationship between two data sets. In Python, dictionaries are a type of data structure that store key/value pairs - perfect!
The 'key' in this instance would be a letter of the alphabet and the 'value' of that key would be its corresponding Morse code. The opposite would be true when converting Morse code back to text.

Python dictionaries are identifiable by their curly braces and colon separated key/value pairs. For instance:


MyDictionary = {"Key":"Value"}


Two dictionaries were created for the final version of the script. The first contained plain text letter 'keys' and Morse code 'values':


TextToMorse = {
"A" : ".-",
"B" : "-...",
"C" : "-.-.",
"D" : "-..",
"E" : ".",
"F" : "..-.",
"G" : "--.",
"H" : "....",
--- snip ---



While the second contained Morse code 'keys' and plain text letter 'values':


MorseToText = {
".-" : "A",
"-..." : "B",
"-.-." : "C",
"-.." : "D",
"." : "E",
"..-." : "F",
"--." : "G",
"...." : "H",

--- snip ---


Manipulating data with string methods 

The biggest challenge encountered while writing this script was data manipulation. Taking the phrase "Attack At Dawn" as an example, the following would need to occur to convert it into Morse code:

1) Each letter of the phrase would need to be separated to give a result like "A t t a c k A t D a w n". This would be necessary to look up each letters key/value pair in the 'TextToMorse' dictionary.

2) The output in Morse code would would need to retain the same letter spacing. Otherwise, it would be impossible to distinguish between ".-",  ".--", and "-" when the output looks like ".---.--.-.".



Data manipulation can be achieved in a number of different ways when using Python. In this instance the string methods join(), split()  and upper() were used to achieve the necessary amounts of data manipulation.

The first of these methods, join(), works in the following manner:



str.join(sequence) 



'Str' and 'sequence' are concatenated against one another to give a new output. As a more readable example lets look at the following code:



def TextToMorse():
    codedmessage = ""
    message = raw_input("Enter a plain text message: ")
    prepmessage = (" ".join(message))





Here the join() method is taking the first string (which happens to be white space) + the string stored in the 'message' variable and concatenating the two to give a new string as its output. This output will then be stored in the 'prepmessage' variable.
Assuming the input to the 'message' variable was the string "Attack At Dawn", than the 'prepmessage' variable would contain the follow string after the join() function had been run: A t t a c k A t D a w n .

The split() method is also worth looking at in detail. Take the following code example:



def MorseToText():
    codedmessage = raw_input("Enter the morse code (single space between letters, double space between words): ")
    decodedmessage = ""

    for i in codedmessage.upper().split(" "):
        decodedmessage += (morsetotext.get(i, " "))




The 'codedmessage' variable is making use of two string methods - upper() and split(). Upper() simply converts the stored string to upper case letters, while split() is being used here to remove white space. Why strip the white space at all? Because the output of plain text letters looks a lot nicer as a formed word as opposed to a space separated word.


Taking user input and script arguments

User input and script arguments are more straight forward to handle. User input in Python can be stored in a variable, like so:



MyName = input("What is your name? ")



When run, the above code would simply prompt the user "What is your name? " and would then store the input in the 'MyName' variable.

Taking script arguments is a little bit more involved but not terribly so. The sys module first needs to be imported into the script so that the sys.argv function can be used. We can then check for user arguments and handle script execution based on the amount of arguments passed to the script. For instance passing the following command to a Windows CMD prompt:



"MorseCodeScript.py" text



will call the script named "MorseCodeScript.py" and pass the argument 'text'. The code would of course need to be written in such a way as to expect 'text' as an argument at this point. If the argument was not passed and the script called, like so:



"MorseCodeScript.py"



than the script may function in an entirely different way yet again, according to how it has been written.

The Python script is written to handle a single argument of either 'text' or 'Morse'. Each of these arguments will invoke a different function written into the script to handle user input in the correct fashion. If no argument is given, a short help section is displayed along with examples of how to use the script:



if __name__ == "__main__":

    if len(sys.argv) < 2:
        print ("Morse code decoder/coder \r\n")
        print ("Usage: " + sys.argv[0] + " (input type) \r\n " +
               "Morse code to Text example: " + sys.argv[0] + " morse \r\n " +
               "Text to Morse code example: " + sys.argv[0] + " text \r\n ")
   
    elif sys.argv[1] == str("morse"):
               MorseToText()

    elif sys.argv[1] == str("text"):
               TextToMorse()

    else:
               print("invalid options selected. Please check your syntax and case. \r\n")
               print ("Usage: " + sys.argv[0] + " < (input type) > \r\n " +
               "Morse code to Text example: " + sys.argv[0] + " morse \r\n " +
               "Text to Morse code example: " + sys.argv[0] + " text \r\n ")




Using both functions of the script 

The final version of code:


TextToMorse = {
"A" : ".-",
"B" : "-...",
"C" : "-.-.",
"D" : "-..",
"E" : ".",
"F" : "..-.",
"G" : "--.",
"H" : "....",
--- snip ---


MorseToText = {
".-" : "A",
"-..." : "B",
"-.-." : "C",
"-.." : "D",
"." : "E",
"..-." : "F",
"--." : "G",
"...." : "H",

--- snip ---

# Convert plain text to Morse code with this function:
def TextToMorse():
    codedmessage = ""
    message = raw_input("Enter a plain text message: ")
    prepmessage = (" ".join(message))

    # Loop over each letter
    for i in prepmessage.upper():
        codedmessage += (morsecode.get(i, " "))

    # Print to console
    print (codedmessage)


#Convert Morse code to plain text with this function:
def MorseToText():
    codedmessage = raw_input("Enter the morse code (single space between letters, double space between words): ")
    decodedmessage = ""

    # Loop over each letter
    for i in codedmessage.upper().split(" "):
        decodedmessage += (morsetotext.get(i, " "))

    # Print to console
    print (decodedmessage)


if __name__ == "__main__":

    # Check for arguments
    if len(sys.argv) < 2:
        print ("Morse code decoder/coder \r\n")
        print ("Usage: " + sys.argv[0] + " (input type) \r\n " +
               "Morse code to Text example: " + sys.argv[0] + " morse \r\n " +
               "Text to Morse code example: " + sys.argv[0] + " text \r\n ")

    # If the argument is "morse"
    elif sys.argv[1] == str("morse"):
               MorseToText()

    # If the argument is "text"
    elif sys.argv[1] == str("text"):
               TextToMorse()

    # If there are no arguments supplied
    else:
               print("invalid options selected. Please check your syntax and case. \r\n")
               print ("Usage: " + sys.argv[0] + " < (input type) > \r\n " +
               "Morse code to Text example: " + sys.argv[0] + " morse \r\n " +
               "Text to Morse code example: " + sys.argv[0] + " text \r\n ")





Calling the script without any arguments displays the help/example section:

Calling the script without an argument

While calling the script with either "Morse" or "text " as the arguments executes the respective code sections:

Morse code to plain text

Plain text to Morse code


Considerations 

A few thoughts on improving the code: Error handling is currently non-existent, which causes the script to ungracefully exit if unexpected input is received. There are also possibly better ways to handle the data manipulation aspect as well (this is part of Python's charm - there is always a better way!).

Given that this was a single afternoon project with several new string methods being researched, practiced and documented, I am certainly happy with the final version of code. Perhaps on my next Sunday afternoon coding challenge I can improve on this success even more!

Comments

Popular posts from this blog

Exploiting OpenSSH 4.7 / OpenSSL 0.9.8 (Metasploitable 2)

Reverse engineering a simple C program part 1: Strings, ltrace, hexdump

Reverse engineering a simple C program part 2 : GDB