Command Line Utilities in Swift

Categories:

This is another post in a series designed to explore the range of applications that Swift can be applied to on Linux server systems.

Our application will build upon a previous example that uses popen combined with the wget command to call a natural language translation service and translate strings from one language to another like Google Translate. Rather than having a one-shot command line that translates a single string, we’re going to create an interactive “shell” that translates each string entered at a prompt. Here’s a screenshot of the application in action:

Translate all the things!
Translate all the things!

The translator prompt indicates what language it is expecting to receive, as well as what language it will attempt to translate to. For example:

  • en->es Translate from English to Spanish
  • es->it Translate from Spanish to Italian
  • it->ru Translate from Italian to Russian

The program defaults to en->es and provides the commands to and from to change the languages. For example, typing to es will change the language to translate to to Spanish. Typing quit will exit the application.

If the string entered is not a command it is taken verbatim and passed to the translation web service. The result of the translation is then echoed back.

What to Note

If you’re coming from a systems or devops programming background and have never been exposed to Swift before, here are the things you should be taking a look at in the code. I think you will find Swift offers a lot for both types of programmers and will become a welcome addition to the Linux programming toolbox.

  • let variableName = value for assigning constants
  • tuples
  • strings in switch-case statements
  • switch-case statements must be exhaustive
  • computed properties
  • import Glibc which provides access to standard C functions
  • the guard statement
  • Foundation classes such as NSThread and NSNotificationCenter
  • posting notifications to trigger code execution in a different object or thread

Application Design

Our translator is broken into a main routine, two classes, and a globals.swift file. If you are going to follow along you should use the Swift Package Manager and lay out your files in a directory like this:

translator/Sources/main.swift
          /Sources/CommandInterpreter.swift
          /Sources/...
          /Package.swift

The main.swift file is a Swift application’s entry point and the only file that should have executable statements in it (assigning a variable or declaring a class is not an “executable statement” in this context).

main.swift:

As it stands our application has no arguments to process off of the command line, so it:

1. Creates an instance of both the CommandInterpreter and Translator class

2. Adds an observer for a notification named InputNotification (we use a constant INPUT_NOTIFICATION defined in globals.swift)

3. Provides a block of code to execute when the notification is received

4. Starts the interpreter

5. Calls select which blocks the main thread while other threads in the application continue to run

CommandInterpreter

The class CommandInterpreter is responsible for reading characters from standard input, parsing the input string to determine what action should be taken, and then taking that action. If you’re new to Swift we’ve commented some of the basic constructs of the language.

CommandInterpreter.swift:

The CommandInterpreter logic is straightforward. When the start function is called an NSThread is created with a block of code that calls fgetc collecting characters on stdin. When the newline character is encountered (the user hits RETURN) the input is parsed and structured into a Command. This in turn is handed over to the doCommand function to be processed.

Our doCommand function is a simple switch-case statement. If a .Quit command is encountered the application simply calls exit(0). The .SetFrom and .SetTo commands are self-explanatory as well. When it comes to the .Translate command, here is where the Foundation notification system is used. doCommand itself doesn’t translate anything but instead posts an application-wide notification named InputNotification. Anyone listening for this notification (our main thread!) will have its associated callback invoked:

As I mentioned in this post, there is a SILgen crash working with NSNotification userInfo dictionaries, so we are cheating with a global variable named translationCommand. In our block we:

1. Decrease our verbosity a little bit by assigning tc to the contents of translationCommand

2. Call the translate function of our Translator object, passing in the required arguments

3. Provide a block to run with the translation is complete, which

4. Uses the nice guard statement of Swift to bail quickly if there was an error

5. Print our translation!

Translator

The Translator was originally introduced in this post, and we have reused it here.

Translator.swift:

Putting it All Together

To pull everything together we have two more files to create: globals.swift and Package.swift:

globals.swift:

Package.swift:

If everything is in its proper place, a swift build should give you a functioning translator application.

swift build
Cloning https://github.com/iachievedit/CJSONC
Using version 1.0.0 of package CJSONC
Cloning https://github.com/PureSwift/CcURL
Using version 1.0.0 of package CcURL
Compiling Swift Module 'translator' (4 sources)
Linking Executable:  .build/debug/translator

If you want to start off with the code already there, grab it from Github and check out the cmdline_translator directory.

Things to Try!

There are a lot of improvements that can be made to the application. Here’s a list of things you might like to try:

  • add command line arguments to set the default from and to language
  • add command line arguments to run in a non-interactive mode
  • add a swap command which swaps the from and to language
  • add a help command
  • collapse the from and to commands into line, like from en to es
  • fix the bug where typing from or to with no arguments results in a crash
  • require the commands to be proceeded with a character like \ (otherwise you can’t translate “quit”)
  • add localizedDescription strings to our Translator errors, or
  • implement throws on the Translator for when errors occur

Closing Thoughts

I am unabashedly a Swift enthusiast and believe it has a great opportunity to stand along side Perl, Python, and Ruby for “devops tasks” as well as compete with C, C++, and Java for “systems programming.” At the moment it is true that for other than the most trivial single file scripts, you must compile your Swift code into a binary. I am hopeful that situation will change and I can move on from languages that start new blocks with whitespace. Folks are already talking about it on the Swift evolution mailing list in this thread.

Leave a Reply

Your email address will not be published. Required fields are marked *