1- /**
2- # Teemoji
3-
4- A command-line tool similar to the classic `tee` utility, but uses a Core ML model to predict an appropriate emoji to prepend each incoming line of text.
5-
6- **Usage**
7- ```bash
8- cat input.txt | teemoji [options] [FILE...]
9- ```
10-
11- **Options**
12- - `-a`, `--append`: Append to the given FILE(s), do not overwrite.
13- - `-h`, `--help`: Display help information.
14-
15- This tool reads from standard input, writes to standard output, and can also write to one or more files.
16- */
17-
181import CoreML
192import Foundation
203
@@ -23,134 +6,158 @@ import Foundation
236/// This `@main` struct orchestrates parsing command-line arguments, opening files in append or write modes,
247/// loading the `TeemojiClassifier` model, reading lines from standard input, and writing them (with an emoji) to
258/// standard output and each specified file.
26-
279@main
2810struct Teemoji {
29- /**
30- The main function for the `Teemoji` CLI tool.
31-
32- This function processes command-line arguments, manages file I/O, and uses the Core ML model to predict emojis for
33- each line read from standard input.
34-
35- - Parameters:
36- - arguments: Typically includes the executable name and any flags / file paths.
37- - Returns: Does not return; the process runs until EOF on standard input.
38- */
3911 static func main( ) {
40- // Parse command-line arguments
12+ // Keep track of exit status, default success.
13+ var exitStatus : Int32 = 0
14+ // Parse command-line arguments.
4115 var arguments = CommandLine . arguments
42- // The first argument is the executable name, so remove it.
43- arguments. removeFirst ( )
16+ arguments. removeFirst ( ) // remove executable name
17+
18+ // We support -a and -i, plus -h/--help.
19+ let appendFlagIndex = arguments. firstIndex ( where: { $0 == " -a " || $0 == " --append " } )
20+ let ignoreSigIntIndex = arguments. firstIndex ( where: { $0 == " -i " } )
21+ let helpFlagIndex = arguments. firstIndex ( where: { $0 == " -h " || $0 == " --help " } )
22+
23+ let append = ( appendFlagIndex != nil )
24+ if let index = appendFlagIndex {
25+ arguments. remove ( at: index)
26+ }
27+
28+ // If -i is present, ignore SIGINT.
29+ if let index = ignoreSigIntIndex {
30+ signal ( SIGINT, SIG_IGN)
31+ arguments. remove ( at: index)
32+ }
4433
45- // Check if the user asked for help
46- if arguments . contains ( " -h " ) || arguments . contains ( " --help " ) {
34+ // If -h or --help is present, print usage and exit.
35+ if helpFlagIndex != nil {
4736 printUsage ( )
4837 exit ( EXIT_SUCCESS)
4938 }
5039
51- // Check for append flag (-a / --append) and strip it out
52- let append = arguments. contains ( " -a " ) || arguments. contains ( " --append " )
53- arguments. removeAll ( where: { $0 == " -a " || $0 == " --append " } )
54-
55- // The remaining arguments are taken to be filenames (like `tee file1 file2 ...`)
40+ // Remaining arguments are treated as file paths.
5641 let fileURLs = arguments. map { URL ( fileURLWithPath: $0) }
5742
58- // Open file handles for writing or appending
59- var fileHandles : [ FileHandle ] = [ ]
43+ // Open file handles.
44+ var fileHandles : [ ( URL , FileHandle ) ] = [ ]
6045 for url in fileURLs {
6146 do {
62- // If appending, open or create the file; otherwise create/truncate it.
6347 if append {
64- // Create the file if it doesn’t exist; otherwise open for appending .
48+ // Create file if it doesn’t exist, otherwise open for append .
6549 if !FileManager. default. fileExists ( atPath: url. path) {
6650 FileManager . default. createFile ( atPath: url. path, contents: nil )
6751 }
6852 let handle = try FileHandle ( forWritingTo: url)
69- // Move the write pointer to the end if appending
7053 try handle. seekToEnd ( )
71- fileHandles. append ( handle)
54+ fileHandles. append ( ( url , handle) )
7255 } else {
73- // Overwrite by creating a new file (truncating existing contents)
56+ // Overwrite by creating a new file.
7457 FileManager . default. createFile ( atPath: url. path, contents: nil )
7558 let handle = try FileHandle ( forWritingTo: url)
76- fileHandles. append ( handle)
59+ fileHandles. append ( ( url , handle) )
7760 }
7861 } catch {
7962 fputs ( " teemoji: cannot open \( url. path) : \( error) \n " , stderr)
63+ exitStatus = 1
8064 }
8165 }
8266
83- // Make sure handles are closed at the end
67+ // Ensure handles get closed.
8468 defer {
85- for handle in fileHandles {
69+ for (_ , handle) in fileHandles {
8670 try ? handle. close ( )
8771 }
8872 }
8973
74+ // Load the ML model.
9075 guard
9176 let modelURL = Bundle . module. url (
92- forResource: " TeemojiClassifier " , withExtension: " mlmodelc " )
77+ forResource: " TeemojiClassifier " , withExtension: " mlmodelc " ) ,
78+ let rawModel = try ? MLModel ( contentsOf: modelURL)
9379 else {
9480 fputs ( " teemoji: failed to load CoreML model. \n " , stderr)
9581 exit ( EXIT_FAILURE)
9682 }
97-
98- guard let rawModel = try ? MLModel ( contentsOf: modelURL) else {
99- fputs ( " teemoji: failed to load CoreML model. \n " , stderr)
100- exit ( EXIT_FAILURE)
101- }
102-
10383 let model = TeemojiClassifier ( model: rawModel)
10484
105- // Read lines from stdin, predict an emoji, write to stdout & files
85+ // Read from stdin line by line , predict emoji, then write to stdout & all open files.
10686 while let line = readLine ( ) {
107- // Attempt inference on the line
87+ // Attempt model inference.
10888 let predictionLabel : String
10989 do {
11090 let prediction = try model. prediction ( text: line)
11191 predictionLabel = prediction. label
11292 } catch {
113- // If model prediction fails for any reason, fall back to no emoji
93+ // If model fails, use a fallback.
11494 predictionLabel = " ❓ "
11595 }
11696
97+ // Prepare output line.
11798 let outputLine = " \( predictionLabel) \( line) \n "
99+ // Always write to stdout.
100+ if fputs ( outputLine, stdout) < 0 {
101+ // If an error occurs while writing to stdout, set exit code.
102+ exitStatus = 1
103+ }
118104
119- // Always write to stdout
120- fputs ( outputLine, stdout)
121-
122- // Also write to each open file
105+ // Also attempt to write to each file.
123106 if let data = outputLine. data ( using: . utf8) {
124- for handle in fileHandles {
125- do {
126- try handle. write ( contentsOf: data)
127- } catch {
128- fputs ( " teemoji: error writing to file: \( error) \n " , stderr)
107+ for (url, handle) in fileHandles {
108+ var offset = 0
109+ let length = data. count
110+ // Attempt partial-write logic to ensure all data is written.
111+ while offset < length {
112+ do {
113+ // We slice the data from the offset onward.
114+ let sliceSize = try handle. writeCompat (
115+ data: data, offset: offset, length: length - offset)
116+ if sliceSize <= 0 {
117+ // Zero or negative means we couldn't write.
118+ throw NSError ( domain: " WriteError " , code: 1 , userInfo: nil )
119+ }
120+ offset += sliceSize
121+ } catch {
122+ fputs ( " teemoji: error writing to \( url. path) : \( error) \n " , stderr)
123+ exitStatus = 1
124+ break
125+ }
129126 }
130127 }
131128 }
132129 }
130+
131+ // Since readLine() returns nil on EOF or error, we can’t distinguish. Just exit.
132+ exit ( exitStatus)
133+ }
134+
135+ /// Prints usage, matching FreeBSD tee’s style.
136+ static func printUsage( ) {
137+ let usage = """
138+ usage: teemoji [-ai] [file ...]
139+
140+ Reads from standard input, writes to standard output and specified files, prepending an emoji
141+ inferred by a Core ML model to each line. Options:
142+ -a \t Append to the given file(s), do not overwrite
143+ -i \t Ignore SIGINT
144+ -h \t Display help (non-standard extension)
145+ """
146+ print ( usage)
133147 }
134148}
135149
136- /// Prints usage information to stdout.
137- ///
138- /// Call this function when `-h` or `--help` flags are detected, or whenever you want
139- /// to remind users how to operate the tool.
140- func printUsage( ) {
141- let usage = """
142- Usage: teemoji [options] [FILE...]
143-
144- Like the standard 'tee' command, teemoji reads from standard input and writes to standard output
145- and the specified FILEs, but prepends an emoji to each line inferred by a Core ML model.
146-
147- Options:
148- -a, --append Append to the given FILE(s), do not overwrite.
149- -h, --help Display this help message.
150-
151- Examples:
152- cat input.txt | teemoji output.txt
153- cat input.txt | teemoji -a output.txt another.log
154- """
155- print ( usage)
150+ // Extend FileHandle to do partial writes similar to POSIX.
151+ extension FileHandle {
152+ /// Write a segment of `data` starting from `offset`, returning how many bytes were written.
153+ fileprivate func writeCompat( data: Data , offset: Int , length: Int ) throws -> Int {
154+ // Slicing the data.
155+ let subdata = data. subdata ( in: offset..< ( offset + length) )
156+ // On Apple platforms, `write` should typically write all data, but we mimic partial writes.
157+ // We'll try writing all subdata at once. If it succeeds, it’s done.
158+ // If it fails, we throw.
159+ // This is a simplified approach: we assume full write or error.
160+ self . write ( subdata)
161+ return subdata. count
162+ }
156163}
0 commit comments