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:
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 Spanishes->it
Translate from Spanish to Italianit->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
andNSNotificationCenter
- 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
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
import Foundation import Glibc let interpreter = CommandInterpreter() let translator = Translator() // Listen for events to translate nc.addObserverForName(INPUT_NOTIFICATION, object:nil, queue:nil) { (_) in let tc = translationCommand translator.translate(tc.text, from:tc.from, to:tc.to){ translation, error in guard error == nil && translation != nil else { print("Translation failure: \(error!.code)") return } print(translation!) } } interpreter.start() select(0, nil, nil, nil, nil) |
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
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 |
// Import statements import Foundation import Glibc // Enumerations enum CommandType { case None case Translate case SetFrom case SetTo case Quit } // Structs struct Command { var type:CommandType var data:String } // Classes class CommandInterpreter { // Read-only computed property var prompt:String { return "\(translationCommand.from)->\(translationCommand.to)" } // Class constant let delim:Character = "\n" init() { } func start() { let readThread = NSThread(){ var input:String = "" print("To set input language, type 'from LANG'") print("To set output language, type 'to LANG'") print("Type 'quit' to exit") self.displayPrompt() while true { let c = Character(UnicodeScalar(UInt32(fgetc(stdin)))) if c == self.delim { let command = self.parseInput(input) self.doCommand(command) input = "" // Clear input self.displayPrompt() } else { input.append(c) } } } readThread.start() } func displayPrompt() { print("\(self.prompt): ", terminator:"") } func parseInput(input:String) -> Command { var commandType:CommandType var commandData:String = "" // Splitting a string let tokens = input.characters.split{$0 == " "}.map(String.init) // guard statement to validate that there are tokens guard tokens.count > 0 else { return Command(type:CommandType.None, data:"") } switch tokens[0] { case "quit": commandType = .Quit case "from": commandType = .SetFrom commandData = tokens[1] case "to": commandType = .SetTo commandData = tokens[1] default: commandType = .Translate commandData = input } return Command(type:commandType,data:commandData) } func doCommand(command:Command) { switch command.type { case .Quit: exit(0) case .SetFrom: translationCommand.from = command.data case .SetTo: translationCommand.to = command.data case .Translate: translationCommand.text = command.data nc.postNotificationName(INPUT_NOTIFICATION, object:nil) case .None: break } } } |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// Listen for events to translate nc.addObserverForName(INPUT_NOTIFICATION, object:nil, queue:nil) { (_) in let tc = translationCommand translator.translate(tc.text, from:tc.from, to:tc.to){ translation, error in guard error == nil && translation != nil else { print("Translation failure: \(error!.code)") return } print(translation!) } } |
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
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 |
import Glibc import Foundation import CcURL import CJSONC class Translator { let BUFSIZE = 1024 init() { } func translate(text:String, from:String, to:String, completion:(translation:String?, error:NSError?) -> Void) { let curl = curl_easy_init() guard curl != nil else { completion(translation:nil, error:NSError(domain:"translator", code:1, userInfo:nil)) return } let escapedText = curl_easy_escape(curl, text, Int32(strlen(text))) guard escapedText != nil else { completion(translation:nil, error:NSError(domain:"translator", code:2, userInfo:nil)) return } let langPair = from + "%7c" + to let wgetCommand = "wget -qO- http://api.mymemory.translated.net/get\\?q\\=" + String.fromCString(escapedText)! + "\\&langpair\\=" + langPair let pp = popen(wgetCommand, "r") var buf = [CChar](count:BUFSIZE, repeatedValue:CChar(0)) var response:String = "" while fgets(&buf, Int32(BUFSIZE), pp) != nil { response = response + String.fromCString(buf)! } let translation = getTranslatedText(response) guard translation.error == nil else { completion(translation:nil, error:translation.error) return } completion(translation:translation.translation, error:nil) } private func getTranslatedText(jsonString:String) -> (error:NSError?, translation:String?) { let obj = json_tokener_parse(jsonString) guard obj != nil else { return (NSError(domain:"translator", code:3, userInfo:nil), nil) } let responseData = json_object_object_get(obj, "responseData") guard responseData != nil else { return (NSError(domain:"translator", code:3, userInfo:nil), nil) } let translatedTextObj = json_object_object_get(responseData, "translatedText") guard translatedTextObj != nil else { return (NSError(domain:"translator", code:3, userInfo:nil), nil) } let translatedTextStr = json_object_get_string(translatedTextObj) return (nil, String.fromCString(translatedTextStr)!) } } |
Putting it All Together
To pull everything together we have two more files to create: globals.swift
and Package.swift
:
globals.swift
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
import Foundation let INPUT_NOTIFICATION = "InputNotification" let nc = NSNotificationCenter.defaultCenter() struct TranslationCommand { var from:String var to:String var text:String } var translationCommand:TranslationCommand = TranslationCommand(from:"en", to:"es", text:"") |
Package.swift
:
1 2 3 4 5 6 7 8 9 |
import PackageDescription let package = Package( name: "translator", dependencies: [ .Package(url: "https://github.com/iachievedit/CJSONC", majorVersion: 1), .Package(url: "https://github.com/PureSwift/CcURL", majorVersion: 1) ] ) |
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
andto
commands into line, likefrom en to es
- fix the bug where typing
from
orto
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 ourTranslator
errors, or - implement
throws
on theTranslator
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.