From 728eefb82498e5bb5275e166829b1335c6984fa0 Mon Sep 17 00:00:00 2001 From: Max Horn Date: Thu, 2 Jun 2022 23:31:43 +0200 Subject: [PATCH] Add `Exec2`, the "better version of Exec" Add a new function `Exec2` which is as easy to use as `Exec` but much more powerful and less easy to misuse: - don't pass everything to a shell, thus avoiding compatibility issues due to different shells on different systems (this also has one downside: you can't use redirects like ">/dev/null") - pass arguments as separate strings, thus avoiding any issues with quoting quotes, quoting spaces, etc. - return the exit code of the executed command so that one can determine its success or failure; the return value is actually a record to allow returning other data, such as the programs output - make it very easy to override the input and output streams - by default, pass no input to the program (instead of passing anything the user might type, as `Exec` does); this can be changed by adding an input stream to the argument list - by default, capture any output of the program into a string and return it, instead of just printing the output to the console; this can be changed by adding an output stream to the argument list --- lib/helpview.gi | 52 +++++++++++++++++++--------------- lib/process.gd | 2 ++ lib/process.gi | 74 +++++++++++++++++++++++++++++++++++++++++++++++++ lib/streams.gi | 11 ++++---- 4 files changed, 111 insertions(+), 28 deletions(-) diff --git a/lib/helpview.gi b/lib/helpview.gi index fedad5787a..93e13322b3 100644 --- a/lib/helpview.gi +++ b/lib/helpview.gi @@ -79,7 +79,7 @@ if ARCH_IS_WINDOWS() then winfilename:=MakeExternalFilename( SplitString( filename, "#" )[1] ); fi; Print( "Opening help page ", winfilename, " in default windows browser ... \c" ); - Exec( Concatenation("start ", winfilename ) ); + Exec2( "start", winfilename ); Print( "done! \n" ); end ); @@ -137,7 +137,7 @@ elif ARCH_IS_MAC_OS_X() then fi; file := file.file; fi; - Exec(Concatenation("open -a Preview ", file)); + Exec2("open", "-a", "Preview", file); Print("# see page ", page, " in the Preview window.\n"); end ); @@ -154,7 +154,7 @@ elif ARCH_IS_MAC_OS_X() then fi; file := file.file; fi; - Exec(Concatenation("open -a \"Adobe Reader\" ", file)); + Exec2("open", "-a", "Adobe Reader", file); Print("# see page ", page, " in the Adobe Reader window.\n"); end ); @@ -171,7 +171,7 @@ elif ARCH_IS_MAC_OS_X() then fi; file := file.file; fi; - Exec(Concatenation("open ", file)); + Exec2("open ", file); Print("# see page ", page, " in the pdf viewer window.\n"); end ); @@ -186,15 +186,15 @@ elif ARCH_IS_MAC_OS_X() then fi; file := file.file; fi; - Exec( Concatenation( - "osascript < ## DeclareGlobalFunction( "Exec" ); + +DeclareGlobalName( "Exec2" ); diff --git a/lib/process.gi b/lib/process.gi index e9f5636e29..c11aa4e3d4 100644 --- a/lib/process.gi +++ b/lib/process.gi @@ -261,3 +261,77 @@ InstallGlobalFunction( Exec, function( arg ) Process( dir, shell, InputTextUser(), OutputTextUser(), [ cs, cmd ] ); end ); + +# TODO: document this, come up with a better name, write some tests... +BindGlobal( "Exec2", function( arg ) + local args, result, a, input, output, dir, cmd; + + args := []; + result := rec(); + + # parse the inputs + for a in arg do + if IsDirectory(a) then + if IsBound(dir) then + Error("must specify at most one working directory"); + fi; + dir := a; + elif IsInputStream(a) then + if IsBound(input) then + Error("must specify at most one input stream"); + fi; + input := a; + elif IsOutputStream(a) then + if IsBound(output) then + Error("must specify at most one output stream"); + fi; + output := a; + elif IsString(a) then + ConvertToStringRep(a); + if not IsBound(cmd) then + cmd := a; + else + Add(args, a); + fi; + else + Error("unsupported argument type"); + fi; + od; + + if not IsBound(cmd) then + Error("must specify a command to execute"); + fi; + + # determine full executable path if it is not already a path + if not '/' in cmd then + a := Filename( DirectoriesSystemPrograms(), cmd ); + if a = fail and ARCH_IS_WINDOWS() then + a := Filename( DirectoriesSystemPrograms(), Concatenation( cmd, ".exe" ) ); + fi; + if a = fail then + Error("could not locate executable for '", cmd, "'"); + fi; + cmd := a; + fi; + + # set default working directory if necessary + if not IsBound(dir) then + dir := DirectoryCurrent(); + fi; + + # if no input stream was specified, pass no input to the command + if not IsBound(input) then + input := InputTextNone(); + fi; + + # if no output stream was specified, put output into the returned record + if not IsBound(output) then + result.output := ""; + output := OutputTextString(result.output, false); + fi; + + # execute the command + result.status := Process( dir, cmd, input, output, args ); + + return result; +end ); diff --git a/lib/streams.gi b/lib/streams.gi index 7a4b207797..9a0feab107 100644 --- a/lib/streams.gi +++ b/lib/streams.gi @@ -1311,19 +1311,20 @@ InstallGlobalFunction( InputFromUser, InstallGlobalFunction( OpenExternal, function(filename) local file; if ARCH_IS_MAC_OS_X() then - Exec(Concatenation("open \"",filename,"\"")); + Exec2("open", filename); elif ARCH_IS_WINDOWS() then - Exec(Concatenation("cmd /c start \"",filename,"\"")); + Exec2("cmd", "/c", "start", filename); elif ARCH_IS_WSL() then # If users pass a URL, make sure if does not get mangled. if ForAny(["https://", "http://"], {pre} -> StartsWith(filename, pre)) then file := filename; else - file := Concatenation("$(wslpath -a -w \"",filename,"\")"); + file := ""; + Exec2("wslpath", "-a", "-w", filename, OutputTextString(file, false)); fi; - Exec(Concatenation("explorer.exe \"", file, "\"")); + Exec2("explorer.exe", file); else - Exec(Concatenation("xdg-open \"",filename,"\"")); + Exec2("xdg-open", filename); fi; end );