Writing command line programs in Swift

Writing command line programs in Swift

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:

  1. translator/Sources/main.swift
  2. /Sources/CommandInterpreter.swift
  3. /Sources/...
  4. ./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 :

  1. import Foundation
  2. import Glibc
  3.    
  4. let interpreter = CommandInterpreter ()
  5. let translator = Translator ()
  6.    
  7. // Listen for events to translate
  8. nc.addObserverForName(INPUT_NOTIFICATION, object:nil, queue:nil) {
  9. (_) in
  10. let tc = translationCommand  
  11. translator.translate(tc.text, from:tc.from, to:tc.to){
  12. translation, error in
  13. guard error == nil && translation != nil else {
  14. print("Translation failure: \(error!.code)")
  15. return
  16. }
  17. print(translation!)
  18. }
  19. }
  20.    
  21. interpreter.start()
  22.    
  23. 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.

  1. // Import statements
  2. import Foundation
  3. import Glibc
  4.    
  5. // Enumerations
  6. enum CommandType {
  7. case None
  8. case Translate
  9. case SetFrom
  10. case SetTo
  11. case Quit
  12. }
  13.    
  14. // Structs
  15. struct Command {
  16. var type:CommandType
  17. var data:String
  18. }
  19.    
  20. // Classes
  21. class CommandInterpreter {
  22.    
  23. // Read-only computed property
  24. var prompt:String {
  25. return "\(translationCommand.from)- > \(translationCommand.to)"
  26. }
  27.    
  28. // Class constant
  29. let delim:Character = "\n"  
  30.    
  31. init() {
  32. }
  33.    
  34. func start() {
  35. let readThread = NSThread (){
  36. var input:String = ""  
  37.         
  38. print("To set input language, type 'from LANG'")
  39. print("To set output language, type 'to LANG'")
  40. print("Type 'quit' to exit")
  41. self.displayPrompt()
  42.    
  43. while true {
  44. let c = Character (UnicodeScalar(UInt32(fgetc(stdin))))
  45. if c == self.delim {
  46. let command = self .parseInput(input)
  47. self.doCommand(command)
  48.            input = "" // Clear input
  49. self.displayPrompt()
  50. } else {
  51. input.append(c)
  52. }
  53. }
  54. }
  55.       
  56. readThread.start()
  57. }
  58.    
  59. func displayPrompt() {
  60. print("\(self.prompt): ", terminator:"")
  61. }
  62.    
  63. func parseInput(input:String) - > Command {
  64. var commandType:CommandType
  65. var commandData:String = ""  
  66.       
  67. // Splitting a string
  68. let tokens = input .characters.split{$ 0 == " "}.map(String.init)
  69.    
  70. // guard statement to validate that there are tokens
  71. guard tokens.count > 0 else {
  72. return Command(type:CommandType.None, data:"")
  73. }
  74.    
  75. switch tokens[0] {
  76. case "quit":
  77.        commandType = .Quit
  78. case "from":
  79.        commandType = .SetFrom
  80.        commandData = tokens [1]
  81. case "to":
  82.        commandType = .SetTo
  83.        commandData = tokens [1]
  84. default:
  85.        commandType = .Translate
  86.        commandData = input  
  87. }
  88. return Command(type:commandType,data:commandData)
  89. }
  90.    
  91. func doCommand(command:Command) {
  92. switch command.type {
  93. case .Quit:
  94. exit(0)
  95. case .SetFrom:
  96.        translationCommand.from = command .data
  97. case .SetTo:
  98.        translationCommand.to = command.data
  99. case .Translate:
  100.        translationCommand.text = command.data
  101. nc.postNotificationName(INPUT_NOTIFICATION, object:nil)
  102. case .None:
  103. break
  104. }
  105. }
  106. }

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):

  1. // Listen for events to translate
  2. nc.addObserverForName(INPUT_NOTIFICATION, object:nil, queue:nil) {
  3. (_) in
  4. let tc = translationCommand  
  5. translator.translate(tc.text, from:tc.from, to:tc.to){
  6. translation, error in
  7. guard error == nil && translation != nil else {
  8. print("Translation failure: \(error!.code)")
  9. return
  10. }
  11. print(translation!)
  12. }
  13. }

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:

  1. import Glibc
  2. import Foundation
  3. import CcURL
  4. import CJSONC
  5.    
  6. class Translator {
  7.    
  8. let BUFSIZE = 1024  
  9.    
  10. init() {
  11. }
  12.    
  13. func translate(text:String, from:String, to:String,
  14. completion:(translation:String?, error:NSError?) - > Void) {
  15.    
  16. let curl = curl_easy_init ()
  17.    
  18. guard curl != nil else {
  19. completion(translation:nil,
  20. error:NSError(domain:"translator", code:1, userInfo:nil))
  21. return
  22. }
  23.    
  24. let escapedText = curl_easy_escape (curl, text, Int32(strlen(text)))
  25.    
  26. guard escapedText != nil else {
  27. completion(translation:nil,
  28. error:NSError(domain:"translator", code:2, userInfo:nil))
  29. return
  30. }
  31.       
  32. let langPair = from + "%7c" + to
  33. let wgetCommand = "wget ​​-qO- http://api.mymemory.translated.net/get\\?q\\=" + String.fromCString(escapedText)! + "\\&langpair\\=" + langPair
  34.       
  35. let pp = popen (wgetCommand, "r")
  36. var buf = [CChar](count:BUFSIZE, repeatedValue:CChar(0))
  37.       
  38. var response:String = ""  
  39. while fgets(&buf, Int32(BUFSIZE), pp) != nil {
  40.        response response = response + String.fromCString(buf)!
  41. }
  42.       
  43. let translation = getTranslatedText (response)
  44.    
  45. guard translation.error == nil else {
  46. completion(translation:nil, error:translation.error)
  47. return
  48. }
  49.    
  50. completion(translation:translation.translation, error:nil)
  51. }
  52.    
  53. private func getTranslatedText(jsonString:String) - > (error:NSError?, translation:String?) {
  54.    
  55. let obj = json_tokener_parse (jsonString)
  56.    
  57. guard obj != nil else {
  58. return (NSError(domain:"translator", code:3, userInfo:nil),
  59. nil)
  60. }
  61.    
  62. let responseData = json_object_object_get (obj, "responseData")
  63.    
  64. guard responseData != nil else {
  65. return (NSError(domain:"translator", code:3, userInfo:nil),
  66. nil)
  67. }
  68.    
  69. let translatedTextObj = json_object_object_get (responseData,
  70. "translatedText")
  71.    
  72. guard translatedTextObj != nil else {
  73. return (NSError(domain:"translator", code:3, userInfo:nil),
  74. nil)
  75. }
  76.    
  77. let translatedTextStr = json_object_get_string (translatedTextObj)
  78.    
  79. return (nil, String.fromCString(translatedTextStr)!)
  80.              
  81. }
  82.    
  83. }

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:

  1.  
  2.  
  3. import Foundation
  4.    
  5. let INPUT_NOTIFICATION = "InputNotification"  
  6. let nc = NSNotificationCenter .defaultCenter()
  7.    
  8. struct TranslationCommand {
  9. var from:String
  10. var to:String
  11. var text:String
  12. }
  13.    
  14. var translationCommand:TranslationCommand = TranslationCommand(from:"en",
  15. to:"es",
  16. text:"")
  17.  
  18. Package.swift:
  19.  
  20. import PackageDescription
  21.    
  22. let package = Package (
  23. name: "translator",
  24. dependencies:
  25. .Package(url: "https://github.com/iachievedit/CJSONC", majorVersion: 1),
  26. .Package(url: "https://github.com/PureSwift/CcURL", majorVersion: 1)
  27. ]
  28. )

If everything is configured correctly, finally execute swift build and a very distinctive translation program will be completed.

  1. swift build
  2. Cloning https://github.com/iachievedit/CJSONC
  3. Using version 1.0.0 of package CJSONC
  4. Cloning https://github.com/PureSwift/CcURL
  5. Using version 1.0.0 of package CcURL
  6. Compiling Swift Module 'translator' (4 sources)
  7. 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.

<<:  Weekly crooked review | IT circles are full of scheming! Qiangdong welcomes a child, and Jack Ma gives another gift

>>:  Android design pattern singleton mode

Recommend

Is the LG G3's camera IMX 214?

Which sensor does the LG G3 camera use? Is it IMX ...

Product growth strategy methodology!

Product strategy is not based on features, but on...

An I/O conference made me understand Google's love for the third world

I wishful thinking that, following a heartwarming...

Why do some missiles have to be erected before they can be launched?

The earliest truly modern missile, the German V-2...

Be careful! If your rice cooker has this problem, don’t use it!

This article was reviewed by Chu Yuhao, PhD from ...

How to choose a download website server?

Download websites mainly provide users with downl...

This wave of QQ updates is much more fun than WeChat's "Tap Tap"

In order to take care of friends who have already...

How to learn to write promotion planning proposals?

Before making a planning proposal, let us first t...