How to Run a Terminal Command in a Swift Script? (E.G. Xcodebuild)

How do I run a terminal command in a Swift script? (e.g. xcodebuild)

If you don't use command outputs in Swift code, following would be sufficient:

#!/usr/bin/env swift

import Foundation

@discardableResult
func shell(_ args: String...) -> Int32 {
let task = Process()
task.launchPath = "/usr/bin/env"
task.arguments = args
task.launch()
task.waitUntilExit()
return task.terminationStatus
}

shell("ls")
shell("xcodebuild", "-workspace", "myApp.xcworkspace")

Updated: for Swift3/Xcode8

Correctly execute shell commands in Swift 5

This is how I do it to start ffmpeg in my app. (ffmpegTask is a Process! instance, declared as an instance property, and prefs.input_uri is a String, coming from user input -It is the URI of an RTSP stream). Hope this helps (the "TemporaryFile" thing is from Ole Begemann's excellent utility):

/* ################################################################## */
/**
This starts the ffmpeg task.

- returns: True, if the task launched successfully.
*/
func startFFMpeg() -> Bool {
ffmpegTask = Process()

// First, we make sure that we got a Process. It's a conditional init.
if let ffmpegTask = ffmpegTask {
// Next, set up a tempdir for the stream files.
if let tmp = try? TemporaryFile(creatingTempDirectoryForFilename: "stream.m3u8") {
outputTmpFile = tmp

// Fetch the executable path from the bundle. We have our copy of ffmpeg in there with the app.
if var executablePath = (Bundle.main.executablePath as NSString?)?.deletingLastPathComponent {
executablePath += "/ffmpeg"
ffmpegTask.launchPath = executablePath
ffmpegTask.arguments = [
"-i", prefs.input_uri,
"-sc_threshold", "0",
"-f", "hls",
"-hls_flags", "delete_segments",
"-hls_time", "4",
outputTmpFile?.fileURL.path ?? ""
]

#if DEBUG
if let args = ffmpegTask.arguments, 1 < args.count {
let path = ([executablePath] + args).joined(separator: " ")
print("\n----\n\(String(describing: path))")
}
#endif

// Launch the task
ffmpegTask.launch()

#if DEBUG
print("\n----\n")
#endif

return ffmpegTask.isRunning
}
}
}

return false
}

Sandboxing is likely to be an issue, depending on where you are calling. ffmpeg doesn't like sandboxes anyway, but I still embed the variant that I build for my app in the same directory as the main app executable.

What Function in swift 5 that ‏ runs/exec an external swift script?

In fact, there is a library that can run external codes in swift Similar like your python Example, you can download the Library by github:

https://github.com/samuelmeuli/swift-exec

How to execute terminal command in swift?

Full examples:

  • go to some directory, let say Desktop
  • Create a file with name swsh, and add into it (plaintext, not rtf, or doc)
#!/usr/bin/env xcrun swift

import Foundation

func shell(launchPath: String, arguments: [String]) -> String {

let process = Process()
process.launchPath = launchPath
process.arguments = arguments

let pipe = Pipe()
process.standardOutput = pipe
process.launch()

let output_from_command = String(data: pipe.fileHandleForReading.readDataToEndOfFile(), encoding: String.Encoding.utf8)!

// remove the trailing new-line char
if output_from_command.characters.count > 0 {
let lastIndex = output_from_command.index(before: output_from_command.endIndex)
return output_from_command[output_from_command.startIndex ..< lastIndex]
}
return output_from_command
}

let output = shell(launchPath: "/bin/date", arguments: [ ])
print(output)

Save, and:

  • open the Terminal
  • type cd ~/Desktop
  • use chmod 755 swsh
  • and run your swift script as: ./swsh

You will get output like:

Sat Mar 25 14:31:39 CET 2017

Edit your swsh and change the shell(... line to:

let output = shell(launchPath: "/usr/bin/env", arguments: [ "date" ])

run it and again will get the date, but now:

  • the swsh executed the /usr/bin/env, (with the argument date)
  • and the /usr/bin/env finds the command date
  • and executed it

Now, create another file in the ~/Desktop and name it as from_swift.

Add into it

echo "Today's date is $(date)"

change the file swsh change the shell line to:

let output = shell(launchPath: "./from_swift", arguments: [ ])

Note, the ./from_swift - using relative path to . (we are in the ~/Desktop directory). Run the swift program:

./swsh

Output:

2017-03-25 14:42:20.176 swift[48479:638098] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'launch path not accessible'

Of course, the script from_swift is not executable yet. So execute:

chmod 755 from_swift
# and run
./swsh

Again error:

2017-03-25 14:45:38.523 swift[48520:639486] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Couldn't posix_spawn: error 8'

This is because the from_swift is a script (not a compiled binary), so the operating system need to know which binary should interpret the script content. Because this is an shell script edit the from_swift script as:

#!/bin/sh
echo "Today's date is $(date)"

Note the added "shebang" line: #!/bin/sh. Run the swift ./swsh and will get

Today's date is Sat Mar 25 14:50:23 CET 2017

Horray, you executed your 1st bash script from swift. ;)

Of course, you can use the /usr/bin/env in the shebang, so now change, the content of the from_swift for example to:

#!/usr/bin/env perl

use strict;
use utf8;
use English;
binmode STDOUT, ":utf8";

printf "The $EXECUTABLE_NAME (ver:$PERL_VERSION) runs me: $0\n";
printf "I ❤️ perl!\n";

Run the ./swsh and will get:

The /usr/bin/perl (ver:v5.18.2) runs me: ./from_swift
I ❤️ perl!

NOTE, we changed nothing in the ./swsh file, just the script file ./from_swift!

All the above done using:

$ uname -a
Darwin nox.local 16.4.0 Darwin Kernel Version 16.4.0: Thu Dec 22 22:53:21 PST 2016; root:xnu-3789.41.3~3/RELEASE_X86_64 x86_64
$ swift --version
Apple Swift version 3.0.2 (swiftlang-800.0.63 clang-800.0.42.1)
Target: x86_64-apple-macosx10.9

So, it is easy to create and execute any script. So, you can enter into your ~/Desktop/from_swift

#!/bin/sh
cd $HOME/Desktop/firebase-mac
npm start

It is possible to do directly from the swsh, (Jens Meder proposed), but using this you got very easy method executing anything from the given script file.

Just remember: the process.launch() executes either:

  • compiled binaries
  • or script files, but the script files

    • must have the shebang line
    • and must be executable using chmod 755 /path/to/script.file

How do I run a terminal command in a Swift script? (e.g. xcodebuild)

If you don't use command outputs in Swift code, following would be sufficient:

#!/usr/bin/env swift

import Foundation

@discardableResult
func shell(_ args: String...) -> Int32 {
let task = Process()
task.launchPath = "/usr/bin/env"
task.arguments = args
task.launch()
task.waitUntilExit()
return task.terminationStatus
}

shell("ls")
shell("xcodebuild", "-workspace", "myApp.xcworkspace")

Updated: for Swift3/Xcode8

How to run and print zsh commands in Swift Executables (like vi)

Turns out, you can use the system() function for C++ in Swift!

First, I created a new target in my package (to get past language mixing errors):

.target(name: "CBridge", dependencies: []),

Then, in the target's source folder, I put the following files:

CBridge.cpp
include/CBridge.h
include/CBridge-Bridging-Header.h

CBridge.cpp

#include "include/CBridge.h"
#include <iostream>
using namespace std;

void shellCMD(const char *cmd) {
system(cmd);
}

CBridge.h

#ifdef __cplusplus
extern "C" {
#endif

void shellCMD(const char *cmd);

#ifdef __cplusplus
}
#endif

CBridge-Bridging-Header.h

#include "CBridge.h"

Simply call shellCMD(command) and it will run it, just like 'system(command)'!

Running terminal commands from Cocoa App in Swift failing: command not found but works using Command Line Tool in swift

Add "--login" as the first task argument:

task.arguments = ["--login", "-c", "dayone2 new 'Hello'"]

and that should fix your error.

Explanation:

When you run Terminal the shell starts up as a login shell, from man sh:

When bash is invoked as an interactive login shell, or as a
non-inter-active shell with the --login option, it first reads and executes commands from the file /etc/profile, if that file exists. After reading that file, it looks for ~/.bash_profile, ~/.bash_login, and ~/.profile, in that order, and reads and executes commands from the first one that exists and is readable.

Among other things the commands in these files typically set the $PATH environment variable, which defines the search path the shell uses to locate a command.

When you run your command line tool in the Terminal it inherits this environment variable and in turn passes it on to the shell it invokes to run your dayone2 command.

When you run a GUI app there is no underlying shell and the $PATH variable is set to the system default. Your error "command not found" indicates that your dayone2 command is not on the default path.

HTH

How to launch a terminal app on PATH in Swift on macOS?

A comment from @MartinR brought me to the right idea. Don't run the terminal command directly, but from a new shell. This way it will do the PATH resolution for you:

    let shellProcess = new Process();
shellProcess.launchPath = "/bin/bash";
shellProcess.arguments = [
"-l",
"-c",
// Important: this must all be one parameter to make it work.
"mysqlsh --py -e 'print(\"Call from shell\")",
];
shellProcess.launch();

This example uses the MySQL shell as example, which is in /usr/local/bin (unlike git, which is in /usr/bin). Git worked already in the beginning, while mysqlsh did not. From the comment you can also see that it is important to make the mysqlsh call a complete and single parameter entry. If you split that then /bin/bash -c will only execute mysqlsh and not pass on the given shell parameters.



Related Topics



Leave a reply



Submit