This is the last part of the general section of the course, and it brings everything so far together from VFP. Your application launches a console program, waits for it to finish, collects what it wrote on the output and the error channel, and checks its code to know whether it went well. With that you can use any tool with a CLI from VFP: git, curl, a compressor, a signing program.
There are three ways to launch it, and each falls short somewhere. At the end there is a function that combines the good parts of all three.
RUN: the usual one
RUN ping -n 2 127.0.0.1 > output.txt
? FILETOSTR("output.txt")From part 1 you already know that RUN waits for the program to finish (and VFP sits frozen meanwhile), that RUN /N does not wait and that with /N the > does nothing. It lacks two things:
- It does not give you the exit code. After a
RUNyou do not know whether the program went well. - It shows the console window while it runs, and in an application that confuses the user.
For a quick test in the Command window, it is fine. For an application, it is not.
WScript.Shell.Run: hidden window, and a code
WScript.Shell is a COM object that comes with Windows. Its Run method takes the command, how to show the window and whether to wait:
LOCAL loShell, lnCode
loShell = CREATEOBJECT("WScript.Shell")
lnCode = loShell.Run("ping -n 1 -w 500 192.0.2.1", 0, .T.)
? lnCodeThe 0 means no window, and the .T., wait for it to finish. With that, Run returns the program’s exit code: a ping to a machine that answers returns 0, and the one above, to a machine that does not exist, 1.
If you do not wait (.F.), Run comes back at once (0.015 seconds on mine) and always returns 0, because the program has not finished yet. That 0 does not mean it went well.
Run does not go through cmd, so redirection and cmd’s own commands (dir, ver, echo) do not work as they are. For that, you put cmd /c in front:
lnCode = loShell.Run("cmd /c ping -n 1 127.0.0.1 > output.txt", 0, .T.)What Run does not give you is the output: if you want it, you have to send it to a file and read that.
WScript.Shell.Exec: output without files, with two problems
Exec launches the program and gives you back an object with its three channels: StdOut, StdErr and StdIn. The output is read without going through any file:
LOCAL loShell, loProcess
loShell = CREATEOBJECT("WScript.Shell")
loProcess = loShell.Exec("git estado")
? "Output:", loProcess.StdOut.ReadAll()
? "Error:", loProcess.StdErr.ReadAll()
DO WHILE loProcess.Status = 0
INKEY(0.05, "H")
ENDDO
? "Code:", loProcess.ExitCodeWith git estado the output arrives empty, the error brings git: 'estado' is not a git command. See 'git --help'. and the code is 1: the three channels from part 3, each on its own. It looks good, but Exec has two serious problems:
- It has no way to hide the window.
Runtakes how to show it;Exechas no such parameter. - It can hang.
StdOut.ReadAll()waits for the program to finish writing on its output. If meanwhile the program writes a lot on the error channel, the space Windows keeps for that channel fills up, the program stops to wait for someone to read it, and nobody reads it because VFP is still waiting for the output. I tried a program that writes 3,000 lines on the error channel: VFP sat waiting until I killed the process by hand.
Exec does not go through cmd either: loShell.Exec("ver") gives error 1429, “The system cannot find the file specified”, because ver is not a program but a cmd command.
A function that combines the good parts
The output comes out of Run by sending it to a temporary file, and the error to another. That way there is no window, no hang, and you have the code:
FUNCTION RunConsole(tcCommand, tcOutput, tcError)
* Runs a console command without showing a window, waits for it to end
* and returns its exit code. The output and the error come back by
* reference (with @), already converted from the console's code page 850
* to VFP's 1252.
LOCAL loShell, lcBase, lcOutputFile, lcErrorFile, lnCode
lcBase = ADDBS(GETENV("TEMP")) + "console-" + SYS(2015)
lcOutputFile = lcBase + "-output.txt"
lcErrorFile = lcBase + "-error.txt"
loShell = CREATEOBJECT("WScript.Shell")
* The outer quotes are for cmd /c, which strips the first and the last:
* that way the command can carry its own. 0: no window. .T.: wait.
lnCode = loShell.Run([cmd /c "] + tcCommand + ;
[ > "] + lcOutputFile + [" 2> "] + lcErrorFile + [""], 0, .T.)
tcOutput = IIF(FILE(lcOutputFile), CPCONVERT(850, 1252, FILETOSTR(lcOutputFile)), "")
tcError = IIF(FILE(lcErrorFile), CPCONVERT(850, 1252, FILETOSTR(lcErrorFile)), "")
ERASE (lcOutputFile)
ERASE (lcErrorFile)
RETURN lnCode
ENDFUNCThis is how it is used:
LOCAL lcOutput, lcError, lnCode
lnCode = RunConsole("ver", @lcOutput, @lcError)
? lnCode, lcOutputThree things about the function that are not obvious at first sight:
- The outer quotes.
cmd /chas an odd rule: if what you pass it starts with a quote, it strips the first and the last quote of the whole line. Without the outer pair, a command that started with a program path in quotes ("C:\...\My programs\curl.exe" ...) lost its final quote and never ran: code 1, no output and no error. With the outer pair,cmdstrips those two and what is inside arrives intact, whatever quotes it has. CPCONVERT, because of part 3: the console writes in code page 850, soverarrives asVersiónand not asVersi¢n. On a US English Windows the console usually uses 437: put the numberchcpgives you.SYS(2015)gives the temporary files a unique name, so two calls at once do not step on each other. And they are deleted at the end.
This is what it returned for each command I gave it:
| Command | Code | What came back |
|---|---|---|
ver | 0 | output: Microsoft Windows [Versión 10.0.26200.9457] |
tasklist /fi "imagename eq explorer.exe" /fo csv /nh | 0 | output: the CSV line for explorer.exe |
git estado | 1 | error: git: 'estado' is not a git command. ... |
a-program-that-does-not-exist | 1 | error: cmd’s “is not recognized as an internal or external command” (in Spanish on mine) |
| a program that writes 3,000 lines on the error channel | 0 | error: the 3,000 lines, in under a second |
The code it returns is cmd’s, which is that of the last program it ran. If you chain several in tcCommand with &, it is the last one’s.
A real example: does that website answer?
curl has come with Windows for years (C:\Windows\System32\curl.exe) and talks to any web server. It lets your application check whether a service answers before sending it anything:
LOCAL lcOutput, lcError, lnCode
lnCode = RunConsole("curl -s -f -o nul --max-time 10 https://irwinrodriguez.dev", @lcOutput, @lcError)
DO CASE
CASE lnCode = 0
? "It answers"
CASE lnCode = 6
? "That server name does not exist"
CASE lnCode = 22
? "It answers, but with an error (404, 500...)"
CASE lnCode = 28
? "No answer within 10 seconds"
OTHERWISE
? "curl failed with code", lnCode
ENDCASEThe options, with what you learned in part 2: -s is a flag that removes curl’s progress bar (without it, the bar goes on the error channel, so it does not dirty the output, but it is not needed); -f makes a web error (a page that does not exist, a server failure) exit with code 22, and -o nul throws away the page content, because you only want to know whether it answers. --max-time 10 sets a limit, and if it is exceeded, the code is 28.
I saw each code come out here: 0 with the website, 22 with a page that does not exist, 6 with a server name that does not exist and 28 with an address that does not answer. Without -f, curl exits with 0 even if the page does not exist: as far as it is concerned, the website answered, even if with a 404. Always read in a program’s help what it considers an error before trusting its code.
Practice
Copy the RunConsole function into a .prg, do SET PROCEDURE TO that file and try from the Command window:
LOCAL lcOutput, lcError
? RunConsole("ver", @lcOutput, @lcError), lcOutput
? RunConsole("where notepad", @lcOutput, @lcError), lcOutput
? RunConsole("git --version", @lcOutput, @lcError), lcOutput
? RunConsole("git estado", @lcOutput, @lcError), lcError
? RunConsole("curl -s -o nul -w %{http_code} https://irwinrodriguez.dev", @lcOutput, @lcError), lcOutput(git --version only works if you have git installed; otherwise the code will be 1 and the error will tell you it cannot find it.)
That is the end of the general section of the course.