Get ffmpeg info from Pipe (stdout) - swift

I want to call ffmpeg to get the duration of a video file. When using the command in OSX Terminal everything works fine:
ffmpeg -i MyVideo.MOV 2>&1 | grep "Duration"
I get this:
Duration: 00:01:23.53, start: 0.000000, bitrate: 39822 kb/s
It is perfect for me. But now I tried this call from within my code:
func shell(launchPath: String, arguments: [String]) -> String
{
let task = Process()
task.launchPath = launchPath
task.arguments = arguments
let pipe = Pipe()
task.standardOutput = pipe
do {
try task.run()
// task.launch() till 10.12, but now catchable!
} catch let error as NSError {
print(error.localizedDescription)
return ""
}
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output: String = NSString(data: data, encoding: String.Encoding.utf8.rawValue)! as String
return output
}
This code works fine for all other external commands. But here I get error:
[NULL # 0x107808800] Unable to find a suitable output format for '2>&1'
2>&1: Invalid argument
I defined the arguments for ffmpeg like this:
let arguments = ["-i", video.path, "2>&1", "|", "grep \"Duration\"" ]
Even if I put them all in one argument as a larger string, it doesn't work. Using "pipe:1" instead of "2>&1" and rest of arguments results also in an error.
Any idea, how I get it to work?

2>&1 means everything in standardError(Output) goes into standardOutput.
Well, you don't really need that, yes?
Why not reading the standardError(Output) directly and do the grep yourself?
let pipe = Pipe()
task.standardError = pipe
... //task.run()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output: String = String(data: data, encoding: .utf8)
By the way, let's avoid NSString and double re-casting into String.
Let's do the grep ourselves too.
let durationLine = output.components(separatedBy: .newlines).first(where: { $0.contains("Duration") })
Steps: Let's get an array of new lines, and find the first line with "Duration".

Related

macOS Swift Launch Location Not Accessible

I want to execute the following shell command from my macOS app:
/Users/macuser/Desktop/videos/ffmpeg -i /Users/macuser/Desktop/videos/vid1.mov -vf fps=1/1 /Users/macuser/Desktop/videos/images/out%0d4.jpg
The method I'm using to do this is:
func shell(_ launchPath: String, _ arguments: [String]) -> String?
{
let task = Process()
task.launchPath = launchPath
task.arguments = arguments
let pipe = Pipe()
task.standardOutput = pipe
task.standardError = pipe
task.launch()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output = String(data: data, encoding: .utf8) ?? ""
task.waitUntilExit()
return output
}
I am passing the arguments to the method like this:
let command = ffmpegLocation
let arguments = [
"-i",
videoLocation,
"-vf",
"fps=1/1",
"\(outputLocation)/out%0d4.jpg"
]
resultsField.stringValue = shell(command,arguments)!
What I get is "launch path is not accessible". I have verified the command does work in Terminal. Do I need to use a different approach?

swift command line tool for git commands, but its no

I would like to create a tool for git commands using swift.
I'm getting git error usage: git [--version] [--help] [-C ]
[-c =].
Is there any way to distribute it as a command-line package and using in the swift file?
import Foundation
struct Task {
static let shared = Task()
func run(with args: String...){
let task = Process()
task.launchPath = "/usr/bin/git"
task.arguments = ["-c", args.joined(separator: " ")]
let pipe = Pipe()
task.standardOutput = pipe
task.launch()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output = String(data: data, encoding: .utf8)!
print(output)
task.waitUntilExit()
}
}
let task = Task.shared
task.run(with: "status")
task.run(with: "fetch --all")
task.run(with: "add --all")
You are using deprecated methods in your code and there are some other things missing.
First we should set the shell to use
func run(with args: String...){
let task = Process()
task.executableURL = URL(fileURLWithPath: "/bin/zsh")
Then instead of using the deprecated launchPath we build a string with the full command and set it as the arguments for the task
let arguments = "/usr/bin/git \(args.joined(separator: " "))"
task.arguments = ["-c", arguments]
I also think it is a good idea to handle any errors by checking standard error
let pipe = Pipe()
let errorPipe = Pipe()
task.standardOutput = pipe
task.standardError = errorPipe
Instead of using the deprecated launch method use run and read both standard out and standard error
do {
try task.run()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
if !data.isEmpty {
if let output = String(data: data, encoding: .utf8) {
print(output)
}
}
let error = errorPipe.fileHandleForReading.readDataToEndOfFile()
if !error.isEmpty {
if let errorMessage = String(data: error, encoding: .utf8) {
print(errorMessage)
}
}
} catch {
print(error)
}
For a simple command it might be worth having the handling of standard output and standard error in an if/else so feel free to change that but for more complicated commands dealing for example with multiple files it might produce both output and errors

Run shell command with pattern in Swift

I would like to run a shell command on many files that should match on a given filename regex. I found this code snippet that runs a shell command with arguments:
func shell(_ arguments: [String] = []) -> String {
let task = Process()
task.launchPath = "/usr/bin/env"
task.arguments = arguments
let pipe = Pipe()
task.standardOutput = pipe
task.launch()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output = String(data: data, encoding: .utf8) ?? "unknown"
return output
}
It runs great, but it does not resolve the parameters:
shell(["ls", "~/Desktop/*.txt"])
Does not resolve the * to all txt files, it tries to only work on a file called *.txt. Is there some option I need to set on Process?
Thanks in advance for your help!
I just found out the answer! The resolving of * and other patterns is done by the shell, Process only runs a given command. So the solution is to create a shell and run the command in there: (will do some clean up in the code, but this works)
shell(["bash", "-c", "ls ~/Desktop/*.txt"])

Swift + terminal

I'm looking for a way to run terminal commands from in Swift (macOS). I came accross this post, but I can't seem to get any of the solutions to work. I am trying to shut down my mac from my app as you can do from terminal (osascript -e 'tell app "loginwindow" to «event aevtrsdn»'), but whenever I do it, I get error: Couldn't posix_spawn: error 13.
I am using this code:
func shell(launchPath: String, arguments: [String] = []) -> (String? , Int32) {
let task = Process()
task.launchPath = launchPath
task.arguments = arguments
let pipe = Pipe()
task.standardOutput = pipe
task.standardError = pipe
task.launch()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output = String(data: data, encoding: .utf8)
task.waitUntilExit()
return (output, task.terminationStatus)
}
and I call it from this:
let z = shell(launchPath: "/usr/bin/osascript", arguments: ["-e", "\'tell app \"loginwindow\" to «event aevtrsdn»\'"])
Any help?
Your code is correct, but you must not enclose the second argument
in single-quotes:
let z = shell(launchPath: "/usr/bin/osascript", arguments: ["-e", "tell app \"loginwindow\" to «event aevtrsdn»"])
That is only necessary when executing a program from the shell.
Process passes the given arguments directly to the spawned executable,
without interpretation by a shell.

How to use Process() in Swift 3 for Linux?

The following function executes a process in Swift 3 on macOS. But if I run the same code in Ubuntu I get the error that Process is an unresolved identifier.
How do I run a process / task in Swift 3 for Ubuntu and get its output?
import Foundation
// runs a Shell command with arguments and returns the output or ""
class func shell(_ command: String, args: [String] = []) -> String {
let task = Process()
task.launchPath = command
task.arguments = args
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 {
// remove whitespaces and newline from start and end
return output.trimmingCharacters(in: .whitespacesAndNewlines)
}
}
return ""
}
I cannot test it myself currently, but according to the source code
https://github.com/apple/swift-corelibs-foundation/blob/master/Foundation/NSTask.swift,
the corresponding class is (still) called Task on Linux, not Process
as on Apple platforms.