[LIB] StdoutToVar

Post your working scripts, libraries and tools.
User avatar
cyruz
Posts: 346
Joined: 30 Sep 2013, 13:31

[LIB] StdoutToVar

Post by cyruz » 06 Oct 2022, 10:23

Hi guys,

I ported last version of the StdoutToVar to Ahk2. I'm just starting with Ahk2 so be advised :)

Code: Select all

; ----------------------------------------------------------------------------------------------------------------------
; Function .....: StdoutToVar
; Description ..: Runs a command line program and returns its output.
; Parameters ...: sCmd - Commandline to be executed.
; ..............: sDir - Working directory.
; ..............: sEnc - Encoding used by the target process. Look at StrGet() for possible values.
; Return .......: Command output as a string on success, empty string on error.
; AHK Version ..: AHK v2 x32/64 Unicode
; Author .......: Sean (http://goo.gl/o3VCO8), modified by nfl and by Cyruz
; License ......: WTFPL - http://www.wtfpl.net/txt/copying/
; Changelog ....: Feb. 20, 2007 - Sean version.
; ..............: Sep. 21, 2011 - nfl version.
; ..............: Nov. 27, 2013 - Cyruz version (code refactored and exit code).
; ..............: Mar. 09, 2014 - Removed input, doesn't seem reliable. Some code improvements.
; ..............: Mar. 16, 2014 - Added encoding parameter as pointed out by lexikos.
; ..............: Jun. 02, 2014 - Corrected exit code error.
; ..............: Nov. 02, 2016 - Fixed blocking behavior due to ReadFile thanks to PeekNamedPipe.
; ..............: Apr. 13, 2021 - Code restyling. Fixed deprecated DllCall types.
; ..............: Oct. 06, 2022 - AHK v2 version. Throw exceptions on failure.
; ..............: Oct. 08, 2022 - Exceptions management and handles closure fix. Thanks to lexikos and iseahound.
; ----------------------------------------------------------------------------------------------------------------------
StdoutToVar(sCmd, sDir:="", sEnc:="CP0") {
    ; Create 2 buffer-like objects to wrap the handles to take advantage of the __Delete meta-function.
    oHndStdoutRd := { Ptr: 0, __Delete: delete(this) => DllCall("CloseHandle", "Ptr", this) }
    oHndStdoutWr := { Base: oHndStdoutRd }
    
    If !DllCall( "CreatePipe"
               , "PtrP" , oHndStdoutRd
               , "PtrP" , oHndStdoutWr
               , "Ptr"  , 0
               , "UInt" , 0 )
        Throw OSError(,, "Error creating pipe.")
    If !DllCall( "SetHandleInformation"
               , "Ptr"  , oHndStdoutWr
               , "UInt" , 1
               , "UInt" , 1 )
        Throw OSError(,, "Error setting handle information.")

    PI := Buffer(A_PtrSize == 4 ? 16 : 24,  0)
    SI := Buffer(A_PtrSize == 4 ? 68 : 104, 0)
    NumPut( "UInt", SI.Size,          SI,  0 )
    NumPut( "UInt", 0x100,            SI, A_PtrSize == 4 ? 44 : 60 )
    NumPut( "Ptr",  oHndStdoutWr.Ptr, SI, A_PtrSize == 4 ? 60 : 88 )
    NumPut( "Ptr",  oHndStdoutWr.Ptr, SI, A_PtrSize == 4 ? 64 : 96 )

    If !DllCall( "CreateProcess"
               , "Ptr"  , 0
               , "Str"  , sCmd
               , "Ptr"  , 0
               , "Ptr"  , 0
               , "Int"  , True
               , "UInt" , 0x08000000
               , "Ptr"  , 0
               , "Ptr"  , sDir ? StrPtr(sDir) : 0
               , "Ptr"  , SI
               , "Ptr"  , PI )
        Throw OSError(,, "Error creating process.")

    ; The write pipe must be closed before reading the stdout so we release the object.
    ; The reading pipe will be released automatically on function return.
    oHndStdOutWr := ""

    ; Before reading, we check if the pipe has been written to, so we avoid freezings.
    nAvail := 0, nLen := 0
    While DllCall( "PeekNamedPipe"
                 , "Ptr"   , oHndStdoutRd
                 , "Ptr"   , 0
                 , "UInt"  , 0
                 , "Ptr"   , 0
                 , "UIntP" , &nAvail
                 , "Ptr"   , 0 ) != 0
    {
        ; If the pipe buffer is empty, sleep and continue checking.
        If !nAvail && Sleep(100)
            Continue
        cBuf := Buffer(nAvail+1)
        DllCall( "ReadFile"
               , "Ptr"  , oHndStdoutRd
               , "Ptr"  , cBuf
               , "UInt" , nAvail
               , "PtrP" , &nLen
               , "Ptr"  , 0 )
        sOutput .= StrGet(cBuf, nLen, sEnc)
    }
    
    ; Get the exit code, close all process handles and return the output object.
    DllCall( "GetExitCodeProcess"
           , "Ptr"   , NumGet(PI, 0, "Ptr")
           , "UIntP" , &nExitCode:=0 )
    DllCall( "CloseHandle", "Ptr", NumGet(PI, 0, "Ptr") )
    DllCall( "CloseHandle", "Ptr", NumGet(PI, A_PtrSize, "Ptr") )
    Return { Output: sOutput, ExitCode: nExitCode } 
}
EDIT:
Oct. 08, 2022 - Updated exceptions management and optimized handles release. Thanks to lexikos and iseahound.
Last edited by cyruz on 09 Oct 2022, 05:35, edited 2 times in total.
ABCza on the old forum.
My GitHub.

iseahound
Posts: 1444
Joined: 13 Aug 2016, 21:04
Contact:

Re: [LIB] StdoutToVar

Post by iseahound » 06 Oct 2022, 17:50

Apparently, throw OSError() is preferred and uses a system generated message
OSError: An internal function call to a Win32 function failed. Message includes an error code and description generated by the operating system. OSErrors have an additional Number property which contains the error code.

burque505
Posts: 1732
Joined: 22 Jan 2017, 19:37

Re: [LIB] StdoutToVar

Post by burque505 » 06 Oct 2022, 18:22

@cyruz thanks for this! I used this as an example, works perfectly with 2.0-beta.10.

Code: Select all

#Include "StdoutToVar.ahk"
cmdline := "cmd /c ping www.google.com"
outvar := StdoutToVar(cmdline)
MsgBox(outvar.Output)
MsgBox(outvar.ExitCode) 
Regards,
burque505

lexikos
Posts: 9583
Joined: 30 Sep 2013, 04:07
Contact:

Re: [LIB] StdoutToVar

Post by lexikos » 07 Oct 2022, 20:19

Throw("Error creating pipe.")
Throwing a String is fine for quick and dirty scripts where you just want to be notified of an unexpected failure, but in general you should throw an Error.

Try and Catch do not suppress or catch thrown non-Error values unless you explicitly use Catch Any or similar, so they won't prevent an error dialog. (If you're using it for debugging, maybe you want the throw to be unhandled anyway.)

By default, the Error constructor saves not only the current file and line, but a stack trace which can be shown in the error dialog. This is retained even if you catch and re-throw the object. (A stack trace shows which function/line called the current function, which one called that, and so on.)

If you use throw OSError(,,"Error setting handle information."), you'll get something like:
Error: (6) The handle is invalid.

Specifically: Error setting handle information.
... which shows both the OS-defined error message indicating the reason for the failure, and a human-readable string indicating where the failure occurred. If you just use throw OSError(), you can still work out where the failure occurred based on the content of the error dialog. You can also use OSError().Message to get the OS-defined error message for whatever purpose.

DllCall( "CloseHandle", "Ptr" , hStdOutWr )
You could avoid the repetition by wrapping the handle in an object with a __Delete method. When the local variable containing the object is freed, __Delete will be called automatically.

DllCall( "Sleep", "UInt",100 )
Using the built-in Sleep instead should make the script more responsive while it waits, such as for hotkeys or the user interacting with the script's main window or GUI. If you call the OS-defined Sleep function, the program cannot respond to messages until the function returns.

User avatar
cyruz
Posts: 346
Joined: 30 Sep 2013, 13:31

Re: [LIB] StdoutToVar

Post by cyruz » 08 Oct 2022, 20:46

iseahound wrote:
06 Oct 2022, 17:50
Apparently, throw OSError() is preferred and uses a system generated message
OSError: An internal function call to a Win32 function failed. Message includes an error code and description generated by the operating system. OSErrors have an additional Number property which contains the error code.
Thanks, I'm gonna fix this soon.

burque505 wrote:
06 Oct 2022, 18:22
@cyruz thanks for this! I used this as an example, works perfectly with 2.0-beta.10.

Code: Select all

#Include "StdoutToVar.ahk"
cmdline := "cmd /c ping www.google.com"
outvar := StdoutToVar(cmdline)
MsgBox(outvar.Output)
MsgBox(outvar.ExitCode) 
Regards,
burque505
Thanks :beer:

lexikos wrote:
07 Oct 2022, 20:19
Throw("Error creating pipe.")
Throwing a String is fine for quick and dirty scripts where you just want to be notified of an unexpected failure, but in general you should throw an Error.

Try and Catch do not suppress or catch thrown non-Error values unless you explicitly use Catch Any or similar, so they won't prevent an error dialog. (If you're using it for debugging, maybe you want the throw to be unhandled anyway.)

By default, the Error constructor saves not only the current file and line, but a stack trace which can be shown in the error dialog. This is retained even if you catch and re-throw the object. (A stack trace shows which function/line called the current function, which one called that, and so on.)

If you use throw OSError(,,"Error setting handle information."), you'll get something like:
Error: (6) The handle is invalid.

Specifically: Error setting handle information.
... which shows both the OS-defined error message indicating the reason for the failure, and a human-readable string indicating where the failure occurred. If you just use throw OSError(), you can still work out where the failure occurred based on the content of the error dialog. You can also use OSError().Message to get the OS-defined error message for whatever purpose.
Thanks for the complete explanation. I'm gonna change it as you suggested.

lexikos wrote:
07 Oct 2022, 20:19
DllCall( "CloseHandle", "Ptr" , hStdOutWr )
You could avoid the repetition by wrapping the handle in an object with a __Delete method. When the local variable containing the object is freed, __Delete will be called automatically.
Something like this would be ok?

Code: Select all

oHndStdoutRd := { Ptr: 0, __Delete: delete(this) => DllCall("CloseHandle", "Ptr", this) }
oHndStdoutWr := { Base: oHndStdoutRd }
It seems to work, but maybe you can advise about the "cleanliness" of this code.

lexikos wrote:
07 Oct 2022, 20:19
DllCall( "Sleep", "UInt",100 )
Using the built-in Sleep instead should make the script more responsive while it waits, such as for hotkeys or the user interacting with the script's main window or GUI. If you call the OS-defined Sleep function, the program cannot respond to messages until the function returns.
:thumbup:
ABCza on the old forum.
My GitHub.

lexikos
Posts: 9583
Joined: 30 Sep 2013, 04:07
Contact:

Re: [LIB] StdoutToVar

Post by lexikos » 08 Oct 2022, 22:03

Something like this would be ok?
Yes.

User avatar
cyruz
Posts: 346
Joined: 30 Sep 2013, 13:31

Re: [LIB] StdoutToVar

Post by cyruz » 09 Oct 2022, 04:44

lexikos wrote:
08 Oct 2022, 22:03
Something like this would be ok?
Yes.
Thanks, I updated the function.
ABCza on the old forum.
My GitHub.

neogna2
Posts: 590
Joined: 15 Sep 2016, 15:44

Re: [LIB] StdoutToVar

Post by neogna2 » 12 Oct 2022, 04:10

Thank you for this cyruz!
For comparison here are other v2 scripts that read StdOut:
RunCMD temp v2 version by SKAN , RunTerminal (clone of RunCMD) by tranht17 , child_process by thqby

User avatar
Brujah4
Posts: 19
Joined: 11 Feb 2023, 03:27

Re: [LIB] StdoutToVar

Post by Brujah4 » 04 Mar 2023, 08:54

@cyruz This seems to be a very nice function. Thanks a lot for this.
But one question, what is the advantage of this function compared to this example in the documentation?

Cheers

User avatar
cyruz
Posts: 346
Joined: 30 Sep 2013, 13:31

Re: [LIB] StdoutToVar

Post by cyruz » 04 Mar 2023, 22:47

Brujah4 wrote:
04 Mar 2023, 08:54
@cyruz This seems to be a very nice function. Thanks a lot for this.
But one question, what is the advantage of this function compared to this example in the documentation?

Cheers
Hi, this is an old function that is around since when AutoHotkey basic was the main version, I think.

Not sure how it compares to the function in the docs, I didn’t know about it and I didn’t carry out any benchmark.

Probably due to the fact that the execution is handled by a COM object, that solution could be more reliable.
ABCza on the old forum.
My GitHub.

Post Reply

Return to “Scripts and Functions (v2)”