How to do a "say" in Swift 3 inside command-line program? - swift

What should I do in this case with the following code?
func convertToM4A(filename: String, voice: String) -> Bool {
let full_string = speaking_queue?.joined(separator: " ")
let command_string: [String] = [/"-v \"\(voice)\"",*/ "--progress", "--output-file=\"\(filename)\"","-i", " \"\(full_string!)\""]
print(command_string)
/
let DocumentsDirectory = FileManager().homeDirectory(forUser: "shyamalchandra")
print((DocumentsDirectory?.absoluteString)!)
*/
let task = Process()
task.launchPath = "/usr/bin/say"
task.arguments = command_string
let pipe = Pipe()
task.standardOutput = pipe
task.launch()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output: String? = String(data: data, encoding: String.Encoding.utf8)
task.waitUntilExit()
if let output = output {
if !output.isEmpty {
print(output.trimmingCharacters(in: .whitespacesAndNewlines))
}
}
return true
}
At run-time, it complains about the TERM environment being not set and furthermore, doesn't write the file to disk. What to do?

The main error is how you build the argument array. The given arguments
are passed directly to the process. Process does not use the shell to
interpret the arguments, therefore you must not enclose them in quotation
marks.
Another problem is that the "-i" (interactive) option cannot be used
when writing to a file.
So your code should look like this:
func convertToM4A(filename: String, voice: String) -> Bool {
let fullString = "Hello world"
let task = Process()
task.launchPath = "/usr/bin/say"
task.arguments = [ "-v", voice, "-o", filename, fullString]
task.launch()
task.waitUntilExit()
return true
}
The "--progress" option causes a progress meter to be displayed on
standard error. If you want to display that then you would have to
read asynchronously from standard error.

If you're writing a native Mac app and want to record synthesized speech to an audio file, don't go trying to wrap a shell command — there's native API
for that. NSSpeechSynthesizer is the macOS API for text-to-speech in general, and it has a method startSpeaking(_:to:) that records output to an audio file.
This API outputs to an AIFF file, but there are numerous APIs you can use to convert/encode that to M4A: AVAssetReader/AVAssetWriter, AVAudioFile, lower-level CoreAudio C APIs, etc.
(Generally, if you're writing a native Mac program and there's something you want to do, check to see if there's an API for it before you go trying to wrap a shell command. Usually those shell commands are using the same API, so you're just punishing yourself with all the indirection, I/O parsing, etc.)
Yes, NSSpeechSynthesizer is an AppKit API, but you can use it in a command line tool.

Take a look at this lib, I've used it before and it is very capable of running shell script. With that then you can use the "say" command and send in some arguments. https://github.com/kareman/SwiftShell
You could try it this way for instance
import SwiftShell
try runAndPrint("say", "Hello world", 4, "arguments")
let array = ["Hello world", "we", "are"]
try runAndPrint("say", array, array.count + 2, "arguments")

Related

How to run terminal command in swift from any directory?

I'm trying to creating a macOS application that that involves allowing the user to run terminal commands. I am able to run a command, but it runs from a directory inside my app, as far as I can tell. Running pwd returns /Users/<me>/Library/Containers/<My app's bundle identifier>/Data.
How can I chose what directory the command runs from?
I'm also looking for a way to get cd to work, but if I can chose what directory to run the terminal command from, I can handle cd manually.
Here is the code that I'm currently using to run terminal commands:
func shell(_ command: String) -> String {
let task = Process()
let pipe = Pipe()
task.standardOutput = pipe
task.arguments = ["-c", command]
task.launchPath = "/bin/zsh"
task.launch()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output = String(data: data, encoding: .utf8)!
return output
}
I'm using Xcode 12 on Big Sur.
Thanks!
There is a deprecated property currentDirectoryPath on Process.
On the assumption you won't want to use a deprecated property, after reading its documentation head over to the FileManager and look at is provisions for managing the current directory and their implications.
Or just use cd as you've considered – you are launching a shell (zsh) with a shell command line as an argument. A command line can contain multiple commands separated by semicolons so you can prepend a cd to your command value.
The latter approach avoids changing your current process' current directory.
HTH
To add to CRD's answer, if using the cd approach, you may also consider separating your commands using && to wait for the previous commands to complete successfully before proceeding to the next command that depends on it.
Try the command you wish to run in the terminal and see if it completes as expected
Eg: /bin/bash -c "cd /source/code/ && git pull && swift build"
If everything works as expected you can go ahead and use it in your swift code as so:
shell("cd /source/code/ && git pull && swift build")
On the topic of deprecations, you may want to replace
launchPath with executableURL
and
launch() with run()
Sample implementation with updated code:
#discardableResult
func shell(_ args: String...) -> Int32 {
let task = Foundation.Process()
task.executableURL = URL(fileURLWithPath: "/bin/bash")
task.arguments = ["-c"]
task.arguments = task.arguments! + args
//Set environment variables
var environment = ProcessInfo.processInfo.environment
environment["PATH"]="/usr/bin/swift"
//environment["CREDENTIALS"] = "/path/to/credentials"
task.environment = environment
let outputPipe = Pipe()
let errorPipe = Pipe()
task.standardOutput = outputPipe
task.standardError = errorPipe
do {
try task.run()
} catch {
// handle errors
print("Error: \(error.localizedDescription)")
}
task.waitUntilExit()
let outputData = outputPipe.fileHandleForReading.readDataToEndOfFile()
let errorData = errorPipe.fileHandleForReading.readDataToEndOfFile()
let output = String(decoding: outputData, as: UTF8.self)
let error = String(decoding: errorData, as: UTF8.self)
//Log or return output as desired
print(output)
print("Ran into error while running: \(error)")
return task.terminationStatus
}

Bash command not found using swift

I created a swift function which runs command in bash which is :
func getConnectedDevices(lblOut: NSTextView)
{
let pipe = Pipe()
let process = Process()
process.launchPath = "/bin/bash"
process.arguments = ["--login", "-c", "mobiledevice get_device_prop DeviceName"]
process.standardOutput = pipe
let fileHandle = pipe.fileHandleForReading
process.launch()
lblOut.string += "\n" + String(data: fileHandle.readDataToEndOfFile(), encoding: .utf8)!
}// Gets all connected iOS Devices
This function works if I just use mobiledevice in command but when I pass proper command to obtain list it gives me error that command not found. I am not very experienced in swift.
The issue resolved by itself. I don't really know how as I didn't change anything at all.

How to create plugins in swift

I found article describing how to create plugin using Swift and Cocoa. It uses NSBundle to load plugin, but that, as far as I know, is not available in pure swift (no Cocoa). Is there way how to achieve same result without using Cocoa?
More info:
In case it's relevant, here is what I want to achieve. I create app in swift that runs on linux server. User can connect to it using their browser. I want to be able to have other people write "plugins" that will implement functionality itself (what user can see and do once they connect), from printing out hello world, through chat programs to games without having to worry about low level stuff provided by my app. Some sort of dll, that my server application loads and runs.
Solution to this is not trivial, but it's not impossible to do either. I prefer to use swift package manager to manage dependencies and Xcode as IDE. This combination is not perfect as it needs a lot of tinkering but there is not any other useable free swift IDE as of now.
You will need to set up two projects, let's call them Plugin (3rd party library) and PluginConsumer (app that uses other people plugins). You will also need to decide on API, for now we will use simple
TestPluginFunc()
Create Plugin.swift file with TestPluginFunc implementation in your Plugin project:
public func TestPluginFunc() {
print("Hooray!")
}
Set the project to build framework, not executable and build[1]. You will get Plugin.framework file which contains your plugin.
Now switch to your PluginConsumer project
Copy Plugin.framework from your Plugin project somewhere where you can easily find it. To actually load the framework and use it:
// we need to define how our plugin function looks like
typealias TestPluginFunc = #convention(c) ()->()
// and what is its name
let pluginFuncName = "TestPluginFunc"
func loadPlugin() {
let pluginName = "Plugin"
let openRes = dlopen("./\(pluginName).framework/\(pluginName)", RTLD_NOW|RTLD_LOCAL)
if openRes != nil {
// this is fragile
let symbolName = "_TF\(pluginName.utf8.count)\(pluginName)\(initFuncName.utf8.count)\(initFuncName)FT_T_"
let sym = dlsym(openRes, symbolName)
if sym != nil {
// here we load func from framework based on the name we constructed in "symbolName" variable
let f: TestPluginFunc = unsafeBitCast(sym, to: TestPluginFunc.self)
// and now all we need to do is execute our plugin function
f()
} else {
print("Error loading \(realPath). Symbol \(symbolName) not found.")
dlclose(openRes)
}
} else {
print("error opening lib")
}
}
If done correctly, you should see "Hooray!" being printed to your log.
There is a lot of room for improvement, first thing you should do is replace Plugin.framework string with parameter, preferably using some file library (I am using PerfectLib). Another thing to look at is defining plugin API in your PluginConsumer project as a protocol or base class, creating framework out of that, importing that framework in your plugin project and basing your implementation on that protocol/base class. I am trying to figure out exactly how to do that. I will update this post if I mange to do it properly.
[1]: I usually do this by creating Package.swift file and creating xcode project out of it using swift package generate-xcodeproj. If your project doesn't contain main.swift, xcode will create framework instead of executable
What you will want to do is create a folder your program will look in. Let's say it's called 'plugins'. It should make a list of names from the files in there, and then iterate through using them, passing parameters to the files and getting the output and making use of that in some way.
Activating a program and getting output:
func runCommand(cmd : String, args : String...) -> (output: [String], error: [String], exitCode: Int32) {
var output : [String] = []
var error : [String] = []
let task = Process()
task.launchPath = cmd
task.arguments = args
let outpipe = Pipe()
task.standardOutput = outpipe
let errpipe = Pipe()
task.standardError = errpipe
task.launch()
let outdata = outpipe.fileHandleForReading.readDataToEndOfFile()
if var string = String(data: outdata, encoding: .utf8) {
string = string.trimmingCharacters(in: .newlines)
output = string.components(separatedBy: "\n")
}
let errdata = errpipe.fileHandleForReading.readDataToEndOfFile()
if var string = String(data: errdata, encoding: .utf8) {
string = string.trimmingCharacters(in: .newlines)
error = string.components(separatedBy: "\n")
}
task.waitUntilExit()
let status = task.terminationStatus
return (output, error, status)
}
`
Here is how a swift plugin would accept arguments:
for i in 1..C_ARGC {
let index = Int(i);
let arg = String.fromCString(C_ARGV[index])
switch arg {
case 1:
println("1");
case 2:
println("2")
default:
println("3)
}
}
So once you have the program and plugin communicating you just have to add handling in your program based on the output so the plugins output can do something meaningful. Without cocoa libraries this seems the way to go, though if you use C there are a couple of other options available there as well. Hope this helps.

Getting input from stdin in a Cocoa command-line app's sub process

I have a command-line app A, and in A I execute an executable script B, in B I'm expecting an input from stdin.
I wrote a demo, implementing A in Swift, using Foundation's Process api, finding that B, no matter implemented in whatever language, cannot get user input from stdin.
Code:
// `A`'s main.swift
import Foundation
let process = Process()
process.launchPath = PATH_TO_SCRIPT_B
process.launch()
process.waitUntilExit()
// `B`
#!/usr/bin/swift
print("intpu something")
let input = readLine()
print("input: \(input)")
I did not set the process's input since according to the doc:
If this method isn’t used, the standard input is inherited from the process that created the receiver.
UPDATE:
A is an executable package created using Swift Package Manager. I used swift package generate-xcodeproj to generate an Xcode project file. I confirmed that if I run the executable built using swift build or xcodebuild in a shell, the problem with getting input from stdin from B arose. However if I run it directly inside Xcode, by pressing command + R inside Xcode, then it worked. So if I understand the difference between running an executable in a shell and Xcode, I can probably make everything work.
func task() {
print("input here")
let x = input()
print ("inputed:" + x)
}
func input() -> String {
let keyboard = FileHandle.standardInput
let inputData = keyboard.availableData
let strData = String(data: inputData, encoding: .utf8)!
let string = strData.trimmingCharacters(in: .newlines)
return string
}
task()
Hope it helps

Use of posix_spawn?

I try to start a command file from my app, so I need to use "posix_spawn". But usage of this requires strange use of pointers. I didn't find any example of Swift (3.0), only C++ or Objective C which I couldn't translate.
I simply need something like that (in old system call):
let err = system("ls -param >file.txt")
Any ideas?
Edit:
The linked solution didn't match.
First it does not use posix_spawn function, which was mentioned by the complier. It uses NSTask, which seems also be abandoned. But I tried this example and ended up in:
func shell(launchPath: String, arguments: [String]) -> String
{
let task = Process()
task.launchPath = launchPath
task.arguments = arguments
let pipe = Pipe()
task.standardOutput = pipe
task.launch()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output: String = NSString(data: data, encoding: String.Encoding.utf8.rawValue)! as String
return output
}
Here all changes needed for XCode 8 were made. But when calling, it will never return from ".launch()".
Somewhere in Output debug window I found this line:
2016-09-15 15:06:36.793 DSRenamer[96562:2569582] launch path not accessible
Same command works fine in terminal window:
/usr/local/bin/ExifTool -ext .CR2
I use swift to call the objective-c menthod(though not the best way)
define an objective-c util menthod
#import "SystemTaskHelper.h"
#implementation SystemTaskHelper
+(void)performSystemTask:(NSString *)task
{
system([task UTF8String]);
}
#end
import objective-c util.h into Bridging-Header.h , and in swift use
SystemTaskHelper.performSystemTask("ls -param >file.txt")