Search code examples
applescriptapplescript-objc

How to get the text output from the terminal?


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


Solution

  • 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