This is one in a series of articles exploring Linux programming in Swift. In the previous example, we used a combination of popen and wget commands to call the natural language translation service to implement a translation function like Google Translate. The program in this article will be based on the work we have done before. But unlike the previous one, which can only translate one sentence each time, this time we have to implement an interactive shell program to translate every sentence entered in the console. Like the screenshot below: The translation program will show what languages it accepts (the source language) and the language it translates to. For example: en->es English to Spanish es->it Spanish to Italian it->ru Italian to Russian The default translation language is en->es, and two commands are provided: to and from to switch between languages. For example, typing to es will set the target language of the translation to Spanish. Typing quit will exit the program. If the string entered by the user is not a command, the translator will send the input verbatim to the translation web service and then print out the returned result. A few points to note If you are a systems or operations programmer and you haven't used Swift before, here are some things to watch out for in your code. I think you'll find that Swift offers a lot of useful features for both types of engineers and will be a welcome new force in the Linux development ecosystem. let variable = value Constant assignment tuples Switch-case supports strings Switch-case must include all cases when used (logical completeness) Computed properties import Glibc can import standard C functions guard statements can use NSThread and NSNotificationCenter classes from Apple's Foundation framework. Trigger the execution of specific code by sending messages in different threads or different objects Programming Our translation program can be split into a main program, two classes, and a globals.swift file. If you plan to follow along, you should use the Swift package manager and adjust your directory structure to look like this: - translator/Sources/main.swift
- /Sources/CommandInterpreter.swift
- /Sources/...
- ./Package.swift
The main.swift file is the entry point of a Swift application and should be the only file that contains executable code (here, things like "assigning a variable" or "declaring a class" do not belong to "executable code"). main.swift : - 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)
The above code indicates that our program does not accept command line parameters. Specific process description: Create instances of the CommandInterpreter and Translator classes respectively and add observers for the InputNotification notification (the constant INPUT_NOTIFICATION used here is defined in globals.swift) Add the code to be executed when the notification is received. Call the start method of the Interpreter class instance and call select to lock the main thread when other threads of the program are running. (Translator's note: that is, to prevent the main thread from terminating prematurely) CommandInterpreter Class The CommandInterpreter class is responsible for reading the input string from the terminal, analyzing the input type and processing it separately. Considering that you may be new to Swift, I have commented on the language features in the code. - // 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 implementation logic of the CommandInterpreter class is very intuitive. When the start function is called, a thread is created through NSThread, and the thread obtains the terminal input through the callback parameter stdin of block fgetc. When a newline character RETURN is encountered (the user presses Enter), the input string will be parsed and mapped into a Command object. Then it is passed to the doCommand function for the rest of the processing. Our doCommand function is a simple switch-case statement. For the .Quit command, we simply call exit(0) to terminate the program. The functions of the .SetFrom and .SetTo commands are obvious. When encountering the .Translate command, Foundation's message system comes in handy. The doCommand function itself does not perform any translation functions, it simply sends an application-level message, that is, InputNotification. Any code that listens for this message will be called (such as our previous main thread): - // 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!)
- }
- }
I mentioned in this article that there will be a SILGen crash when doing type conversion on the userInfo dictionary of NSNotification. Here we use a global variable called translationCommand to bypass this crash. In this code: For the sake of code simplicity, assign the content of translationCommand to tc Call the translate method of the Translator object and pass in the relevant parameters to implement the callback after the translation is completed. A beautiful Swift guard statement is used to detect whether there is an error and return the translated text. Translator The Translator class was originally introduced in this article, and we reuse it here directly: - 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 = 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 the pieces together To tie the components described above together, we need to create two additional files: globals.swift and Package.swift. globals.swift: -
-
- 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:
-
- 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 configured correctly, finally execute swift build and a very distinctive translation program will be completed. - 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
Try Do It Yourself There are still many areas where the current translation program can be improved. Here is a list of things you can try: - Accepts command line arguments to set the default source and target languages
- Accepts command line arguments to enable non-interactive mode
- Added swap command to exchange source and target languages
- Add help command
- Integrate the from command and the to command. You can set both in one line, such as from en to es
- Now when you enter the from command and the to command, if you don't enter the corresponding language at the same time, it will crash. Fix this BUG
- Implement the processing of the escape character \, so that the program's "command" can also be translated (such as the exit command: quit)
- Add localized support for error messages via localizedDescription
- Implemented in the Translator class but when an error occurs, handle the exception through throws
Conclusion Try Do It Yourself There are still many areas where the current translation program can be improved. Here is a list of things you can try: - Accepts command line arguments to set the default source and target languages
- Accepts command line arguments to enable non-interactive mode
- Added
swap command to exchange source and target languages - Add
help command - Integrate
from command and the to command. You can set both commands in one line, such as from en to es - Now when you enter the
from command and to command, if you don't enter the corresponding language at the same time, it will crash. Fix this BUG - Implement the processing of the escape character
\ , so that the program's "command" can also be translated (such as the exit command: quit) - Add localized support for error messages via
localizedDescription - When an error occurs in
Translator class, the exception is handled by throws .
Conclusion I have never concealed that I am an avid Swift fan, and I firmly believe that it is likely to be as good as Perl, Python and Ruby in terms of operation and maintenance, and as good as C, C++ and Java in terms of system programming. I know that compared with those single-file scripting languages, Swift is a bit annoying because it must be compiled into binary files. I sincerely hope that this can be improved so that I can stop focusing on the language level and do something new and cool. I sincerely hope this improves so I can stop focusing on the language and start making new, cool stuff. |