I'm trying to run a Process from my Swift macOS app. But, I'm having trouble formatting the arguments to my Process.
So far, the following code is working well:
let task = Process()
var environment = ProcessInfo.processInfo.environment
environment["PATH"] = "/usr/local/bin"
task.environment = environment
task.currentDirectoryPath = "/usr/local/bin"
task.executableURL = URL(fileURLWithPath: "/usr/local/bin/convert")
task.arguments = [filename, filename+".pdf"]
let outputPipe = Pipe()
let errorPipe = Pipe()
task.standardOutput = outputPipe
task.standardError = errorPipe
...
try! task.run()
When I try and add the arguments "-trim", "-fuzz 10%" to the existing task.arguments the program crashes. The process error is:
New error: convert: unrecognized option `-fuzz 10%' # error/convert.c/ConvertImageCommand/1718.
How do I properly format these arguments to run the process?
When you run commands on the command line, your shell typically splits arguments to pass to a program by whitespace — when you run a command like
/usr/local/bin/convert -trim -fuzz 10% <input> <output>
the shell will split those arguments and pass them along to convert like so:
["/usr/local/bin/convert", "-trim", "-fuzz", "10%", "<input>", "<output>"]
When convert goes to parse the arguments you pass in, it iterates over those arguments and attempts to match them against a known list that it accepts. It looks up -trim, which it recognizes, and -fuzz, which it recognizes, and it accepts 10% as the argument to -fuzz... and so on.
When you execute a program directly from your code, whether through Process, or the lower level execve or posix_spawn, the arguments that you pass in are forwarded directly to the receiving process. In your original case, this means that convert received:
["/usr/local/bin/convert", "-trim", "-fuzz 10%", "<input>", "<output>"]
convert, and many other programs, don't further split up arguments on whitespace themselves, so when it tried to match the entirety of -fuzz 10% as a single command line option (space and all), it couldn't find anything.
This is equivalent to running
/usr/local/bin/convert -trim "-fuzz 10%" <input> <output>
on the command line, where the quotes prevent your shell from splitting words on whitespace. When I run that command locally on my machine, I get the exact same error:
convert: unrecognized option `-fuzz 10%' # error/convert.c/ConvertImageCommand/1718.
The solution is to do the same splitting your shell might do, and pass in each argument separately as its own string:
task.arguments = ["-trim", "-fuzz", "10%", filename, filename+".pdf"]
Related
To run AppleScript using processes in Swift is needed something like this
let process = Process()
if process.isRunning == false {
let pipe = Pipe()
process.launchPath = "/usr/bin/osascript"
process.arguments = ["/Users/user/Documents/activateApplication.scpt"]
process.standardError = pipe
process.launch()
}
but how, using processes I can send more arguments? like "Safari" and use this arguments in the destination script?
the activateApplication.scpt is:
on run appName
tell application appName to activate
end run
This is documented in the osascript man page. Pass the script arguments as additional arguments after the script file, and they will appear as a list in the first (and only) argument of your run handler. Your Swift code would look something like this:
process.arguments = ["/Users/user/Documents/activateApplication.scpt", "Safari"]
...and your script would look like this:
on run argv
tell app (item 1 of argv) to activate
end
In Swift 4, I try to launch a command line to know the frame rate of a video. I use mediainfo tool.
The command to execute is (tested in Terminal)
"/Users/Lorenzo/mediainfo --Inform="Video;%FrameRate%" /Users/Lorenzo/Desktop/1.mov"
And my swift code for that purpose is:
let taskfindfps = Process()
taskfindfps.launchPath = "/Users/Lorenzo/mediainfo"
taskfindfps.arguments = ["--Inform=\"Video;%FrameRate%\"", myVideo]
let pipefindfps = Pipe()
taskfindfps.standardOutput = pipefindfps
But the first argument isn't valid, and I don't know why...
The result I have is like the result of the command without the optional argument:
"/Users/Lorenzo/mediainfo /Users/Lorenzo/Desktop/1.mov"
Is there something wrong in "--Inform=\"Video;%FrameRate%\"" ?
Without knowing Swift exactly, I would try without escaped quotes, quotes are used on e.g. command line only for forcing the command to not handle the semi column as something for the command line (the command line removes them during processing. Process.arguments being a list, the language is expected to handle correctly itself characters to escape, and if it escapes quotes (instead of processing them as it is done on the command line) MediaInfo will not understand the command.
Jérôme, developer of MediaInfo.
You are right, I have my fps number!
With the code:
taskfindfps.arguments = [ "--Inform=Video;%FrameRate%", myVideo]
Thank you for your help.
And thanks for the powerful MediaInfo tool.
I am trying to read the contents of a URL synchronously for a simple command-line batch script in Swift. I am using cURL for simplicity's sake - I know I could use NSURLSession if I had to. I am also building this with swift build using the open-source version of Swift on OSX.
The problem is that on certain URLs, the NSTask never terminates, if stdout has been redirected to a pipe.
// This will hang, and when terminated with Ctrl-C reports "(23) Failed writing body"
import Foundation
let task = NSTask()
let pipe = NSPipe()
task.launchPath = "/usr/bin/curl"
task.arguments = ["http://trove.nla.gov.au/newspaper/page/21704647"]
task.standardOutput = pipe
task.launch()
task.waitUntilExit()
However, if you remove the pipe, or change the URL, the task succeeds.
// This will succeed - no pipe
import Foundation
let task = NSTask()
task.launchPath = "/usr/bin/curl"
task.arguments = ["http://trove.nla.gov.au/newspaper/page/21704647"]
task.launch()
task.waitUntilExit()
// This will succeed - different URL
import Foundation
let task = NSTask()
let pipe = NSPipe()
task.launchPath = "/usr/bin/curl"
task.arguments = ["http://trove.nla.gov.au/newspaper/page/21704646"]
task.standardOutput = pipe
task.launch()
task2.waitUntilExit()
Running any of the examples directly using curl from Terminal succeeds, so there is something about the interaction with NSTask, when retrieving from that specific URL (and a few others), and when a pipe is present, that is causing cURL to fail.
Expanding a little on #Hod's answer: The standard output of the launched
process is redirected to a pipe, but your program never reads from the
other pipe end. A pipe has a limited buffer, see for example
How big is the pipe buffer?
which explains that the pipe buffer size on macOS is (at most) 64KB.
If the pipe buffer is full then the launched process cannot write on it
anymore. If the process uses blocking I/O then a write() to the pipe will block until until at least one byte can be written. That does
never happen in your case, so the process hangs and does not terminate.
The problem can occur only if the amount written to standard output
exceeds the pipe buffer size, which explains why it happens only with certain URLs and not with others.
As a solution, you can read from the pipe, e.g. with
let data = pipe.fileHandleForReading.readDataToEndOfFile()
before waiting for the process to terminate. Another option is to
use asynchronous reading, e.g. with the code from Real time NSTask output to NSTextView with Swift:
pipe.fileHandleForReading.readabilityHandler = { fh in
let data = fh.availableData
// process data ...
}
That would also allow to read both standard output and standard error
from a process via pipes without blocking.
Both curl and NSPipe buffer data. Based on the error you're getting when you ctrl-c out (which indicates curl couldn't write the expected amount of data), you've got a bad interaction between these.
Try adding the -N option to curl to prevent it from buffering its output.
curl can also output progress. I don't think that's causing a problem, but you might add -s to only get the data just in case.
I am trying to read the contents of a URL synchronously for a simple command-line batch script in Swift. I am using cURL for simplicity's sake - I know I could use NSURLSession if I had to. I am also building this with swift build using the open-source version of Swift on OSX.
The problem is that on certain URLs, the NSTask never terminates, if stdout has been redirected to a pipe.
// This will hang, and when terminated with Ctrl-C reports "(23) Failed writing body"
import Foundation
let task = NSTask()
let pipe = NSPipe()
task.launchPath = "/usr/bin/curl"
task.arguments = ["http://trove.nla.gov.au/newspaper/page/21704647"]
task.standardOutput = pipe
task.launch()
task.waitUntilExit()
However, if you remove the pipe, or change the URL, the task succeeds.
// This will succeed - no pipe
import Foundation
let task = NSTask()
task.launchPath = "/usr/bin/curl"
task.arguments = ["http://trove.nla.gov.au/newspaper/page/21704647"]
task.launch()
task.waitUntilExit()
// This will succeed - different URL
import Foundation
let task = NSTask()
let pipe = NSPipe()
task.launchPath = "/usr/bin/curl"
task.arguments = ["http://trove.nla.gov.au/newspaper/page/21704646"]
task.standardOutput = pipe
task.launch()
task2.waitUntilExit()
Running any of the examples directly using curl from Terminal succeeds, so there is something about the interaction with NSTask, when retrieving from that specific URL (and a few others), and when a pipe is present, that is causing cURL to fail.
Expanding a little on #Hod's answer: The standard output of the launched
process is redirected to a pipe, but your program never reads from the
other pipe end. A pipe has a limited buffer, see for example
How big is the pipe buffer?
which explains that the pipe buffer size on macOS is (at most) 64KB.
If the pipe buffer is full then the launched process cannot write on it
anymore. If the process uses blocking I/O then a write() to the pipe will block until until at least one byte can be written. That does
never happen in your case, so the process hangs and does not terminate.
The problem can occur only if the amount written to standard output
exceeds the pipe buffer size, which explains why it happens only with certain URLs and not with others.
As a solution, you can read from the pipe, e.g. with
let data = pipe.fileHandleForReading.readDataToEndOfFile()
before waiting for the process to terminate. Another option is to
use asynchronous reading, e.g. with the code from Real time NSTask output to NSTextView with Swift:
pipe.fileHandleForReading.readabilityHandler = { fh in
let data = fh.availableData
// process data ...
}
That would also allow to read both standard output and standard error
from a process via pipes without blocking.
Both curl and NSPipe buffer data. Based on the error you're getting when you ctrl-c out (which indicates curl couldn't write the expected amount of data), you've got a bad interaction between these.
Try adding the -N option to curl to prevent it from buffering its output.
curl can also output progress. I don't think that's causing a problem, but you might add -s to only get the data just in case.
I am trying to read the contents of a URL synchronously for a simple command-line batch script in Swift. I am using cURL for simplicity's sake - I know I could use NSURLSession if I had to. I am also building this with swift build using the open-source version of Swift on OSX.
The problem is that on certain URLs, the NSTask never terminates, if stdout has been redirected to a pipe.
// This will hang, and when terminated with Ctrl-C reports "(23) Failed writing body"
import Foundation
let task = NSTask()
let pipe = NSPipe()
task.launchPath = "/usr/bin/curl"
task.arguments = ["http://trove.nla.gov.au/newspaper/page/21704647"]
task.standardOutput = pipe
task.launch()
task.waitUntilExit()
However, if you remove the pipe, or change the URL, the task succeeds.
// This will succeed - no pipe
import Foundation
let task = NSTask()
task.launchPath = "/usr/bin/curl"
task.arguments = ["http://trove.nla.gov.au/newspaper/page/21704647"]
task.launch()
task.waitUntilExit()
// This will succeed - different URL
import Foundation
let task = NSTask()
let pipe = NSPipe()
task.launchPath = "/usr/bin/curl"
task.arguments = ["http://trove.nla.gov.au/newspaper/page/21704646"]
task.standardOutput = pipe
task.launch()
task2.waitUntilExit()
Running any of the examples directly using curl from Terminal succeeds, so there is something about the interaction with NSTask, when retrieving from that specific URL (and a few others), and when a pipe is present, that is causing cURL to fail.
Expanding a little on #Hod's answer: The standard output of the launched
process is redirected to a pipe, but your program never reads from the
other pipe end. A pipe has a limited buffer, see for example
How big is the pipe buffer?
which explains that the pipe buffer size on macOS is (at most) 64KB.
If the pipe buffer is full then the launched process cannot write on it
anymore. If the process uses blocking I/O then a write() to the pipe will block until until at least one byte can be written. That does
never happen in your case, so the process hangs and does not terminate.
The problem can occur only if the amount written to standard output
exceeds the pipe buffer size, which explains why it happens only with certain URLs and not with others.
As a solution, you can read from the pipe, e.g. with
let data = pipe.fileHandleForReading.readDataToEndOfFile()
before waiting for the process to terminate. Another option is to
use asynchronous reading, e.g. with the code from Real time NSTask output to NSTextView with Swift:
pipe.fileHandleForReading.readabilityHandler = { fh in
let data = fh.availableData
// process data ...
}
That would also allow to read both standard output and standard error
from a process via pipes without blocking.
Both curl and NSPipe buffer data. Based on the error you're getting when you ctrl-c out (which indicates curl couldn't write the expected amount of data), you've got a bad interaction between these.
Try adding the -N option to curl to prevent it from buffering its output.
curl can also output progress. I don't think that's causing a problem, but you might add -s to only get the data just in case.