0

I would like to know if it is possible to get the text output that is displayed in the terminal by running a shell script and display it in a Scrollable Text View, using applescript. for example:

The output that the command: git clone https://github.com/torvalds/linux.git displays as shown in the image below would be displayed in a Scrollable Text view, would that be possible?

P.S:I'm sorry if the explanation was not clear, I hope someone understands and can help me!! enter image description here

TheOnlyOneHere
  • 115
  • 1
  • 10
  • Check out Apple's Technical Note [TN2065](https://developer.apple.com/library/archive/technotes/tn2065/_index.html) which includes an explanation of how to sometimes get the text returned from a shell command. I'm not familiar with the output of that command so I don't want to hazard a guess. – Mockman Dec 24 '22 at 03:16
  • If you are talking about including stuff like the [escape codes](https://en.wikipedia.org/wiki/ANSI_escape_code) used for positioning the cursor, you would need to emulate a terminal. – red_menace Dec 24 '22 at 05:55
  • @red_menace, I'm wanting to get the terminal output generated by a certain shell script, something as described here: https://stackoverflow.com/questions/29514738/get-terminal-output-after-a-command-swift – TheOnlyOneHere Dec 24 '22 at 15:02
  • A better example would be https://stackoverflow.com/questions/3284782/how-to-gradually-retrieve-data-from-nstask/, but you will be getting each individual line, not what the Terminal is doing - it uses control codes to manipulate the cursor for the progress. Also note that `git-clone` sends its progress on StandardError. – red_menace Dec 26 '22 at 18:05
  • @red_menace, I have no idea how to convert OBJ-c code to Applescript :( – TheOnlyOneHere Dec 30 '22 at 00:47

1 Answers1

1

The steps for getting output from an asynchronous task like this are:

  • Create an NSTask;
  • set its output to an NSPipe's fileHandleForReading;
  • register for a notification so you can get data to put into the textView as it becomes available.

To help with converting from Objective-C, Apple provided a conversion guide with their AppleScriptObjC Release Notes, but other than examples posted on various web sites and forums, that is about it. In general, for specific information about the various Cocoa classes and methods, you will need to look them up in Apple's documentation (for Swift you can switch to the Objective-C equivalent).

Note that an NSTextView does not have any terminal emulation (ANSI escape codes, etc), which is not trivial (take a look at iTerm2 for an example terminal application), so there won't be any cursor control. Git is also a little weird in that the progress uses standard error, so that will need to be redirected to standard output.

For a plain Xcode example, create a new AppleScriptObjC project and add the following statements to the AppDelegate:

property textView : missing value -- IBOutlet
property task : missing value -- this will be the NSTask

to startTask()
   tell current application's NSTask's alloc's init() -- set up the task
      its setCurrentDirectoryURL:(current application's NSURL's fileURLWithPath:(POSIX path of (path to desktop folder))) -- currentDirectoryPath deprecated in 10.13
      set gitPath to "/Applications/Xcode.app/Contents/Developer/usr/bin/git"
      its setExecutableURL:(current application's NSURL's fileURLWithPath:"/bin/zsh") -- launchPath deprecated in 10.13
      its setArguments:{"-c", gitPath & " clone --progress https://github.com/torvalds/linux.git 2>&1"} -- combine stderr with stdout
      its setStandardOutput:(current application's NSPipe's pipe())
      its standardOutput's fileHandleForReading's readInBackgroundAndNotify()
      set my task to it -- update script property
   end tell
   # set up notification observers
   set notificationCenter to current application's NSNotificationCenter's defaultCenter
   set readNotification to current application's NSFileHandleReadCompletionNotification
   notificationCenter's addObserver:me selector:"dataAvailable:" |name|:readNotification object:(task's standardOutput's fileHandleForReading)
   set terminateNotification to current application's NSTaskDidTerminateNotification
   notificationCenter's addObserver:me selector:"taskTerminated:" |name|:terminateNotification object:task
   set {theResult, theError} to task's launchAndReturnError:(reference) -- |launch| deprecated in 10.13
   if theError is missing value then
      log "Task Launched"
   else
      log "Error launching task:  " & (theError's localizedDescription() as text)
   end if
end startTask

on dataAvailable:notification -- get some output from the task
   set theData to notification's userInfo()'s objectForKey:(current application's NSFileHandleNotificationDataItem)
   if theData is not missing value and theData's |length|() > 0 then showResult(theData)
   notification's object's readInBackgroundAndNotify() -- notify again when more data is available
end dataAvailable:

to showResult(resultData) -- append data to the end of the text view
   set resultString to current application's NSString's alloc()'s initWithData:resultData encoding:(current application's NSUTF8StringEncoding)
   set attributedString to current application's NSMutableAttributedString's alloc()'s initWithString:resultString
   set theFont to (current application's NSFont's fontWithName:"Menlo Regular" |size|:12)
   set theRange to (current application's NSMakeRange(0, attributedString's |length|()))
   attributedString's addAttribute:(current application's NSFontAttributeName) value:theFont range:theRange -- use monospaced font
   textView's textStorage()'s appendAttributedString:attributedString
   textView's scrollToEndOfDocument:me -- 10.14+
end showResult

on taskTerminated:notification
   current application's NSNotificationCenter's defaultCenter's removeObserver:me
   repeat -- get any early termination leftovers
      set theData to notification's object's standardOutput's fileHandleForReading's availableData
      if theData is not missing value and theData's |length|() > 0 then
         showResult(theData)
      else
         exit repeat
      end if
   end repeat
   set my task to missing value -- clear script property
   log "Task Terminated"
end taskTerminated:

In the Interface Editor, add a scrollable text view to the main window and connect it to the textView property, edit the currentDirectory and gitPath locations as needed, and put a statement in the applicationWillFinishLaunching hander to call startTask().

For something a bit simpler (shorter) to test with that still has a little output, the task arguments can be changed to something like:

   its setExecutableURL:(current application's NSURL's fileURLWithPath:"/usr/sbin/system_profiler")
   its setArguments:{"-detailLevel", "basic"} -- mini, basic, full
   -- or --
   its setExecutableURL:(current application's NSURL's fileURLWithPath:"/bin/zsh")
   its setArguments:{"-c", "find /Users/$USER -iname '*.scpt'" } -- find scripts
red_menace
  • 3,162
  • 2
  • 10
  • 18
  • Can I pass a shell script instead of the gi clone command? for example I have some shell script with some commands and get the output of these commands in the TextView? – TheOnlyOneHere Dec 31 '22 at 14:28
  • 1
    @TheOnlyOneHere - I have only used single lines, but when using `bash` or `zsh` with the `-c` option you can use a shell script for the argument. Be careful of security issues if allowing users to enter commands. I've updated my answer to include another example. Also note that you don't need to set the current directory if it isn't needed. – red_menace Dec 31 '22 at 16:15