On 13.04.23 16:18, Waldek Hebisch wrote:
Of course, all needed redirections and buffering could be hidden in
an utility function. AFAICS need to run external programs and
capture output is rare enough that nobody bothered to write a
special function.
I have written a simple stupid interface that can call any external program.
I can call Sage via
)compile runextcmd.spad
RUN ==> run $ RunExternalCommand
out := RUN("/bin/sh sage.sh", "[x^2 for x in range(40)]")
or
setExecutable("sage", "/bin/sh sage.sh")
out := RUN("sage", "[x^2 for x in range(40)]")
This will give the value:
(5)
["[0,", " 1,", " 4,", " 9,", " 16,", " 25,", " 36,", " 49,", " 64,",
" 81,",
" 100,", " 121,", " 144,", " 169,", " 196,", " 225,", " 256,", "
289,",
" 324,", " 361,", " 400,", " 441,", " 484,", " 529,", " 576,", "
625,",
" 676,", " 729,", " 784,", " 841,", " 900,", " 961,", " 1024,", "
1089,",
" 1156,", " 1225,", " 1296,", " 1369,", " 1444,", " 1521]"]
Type: List(String)
Of course, that needs to be brought back into proper FriCAS objects like
str := concat out
(6)
"[0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121, 144, 169, 196, 225,
256, 289,
324, 361, 400, 441, 484, 529, 576, 625, 676, 729, 784, 841, 900,
961, 1024,
1089, 1156, 1225, 1296, 1369, 1444, 1521]"
(7) -> inf := parse(str)$InputForm
(7)
(construct 0 1 4 9 16 25 36 49 64 81 100 121 144 169 196
225 256 289 324 361 400 441 484 529 576 625 676 729 784
841
900 961 1024 1089 1156 1225 1296 1369 1444 1521)
Type: InputForm
(8) -> interpret(inf)
(8)
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121, 144, 169, 196, 225,
256, 289,
324, 361, 400, 441, 484, 529, 576, 625, 676, 729, 784, 841, 900,
961, 1024,
1089, 1156, 1225, 1296, 1369, 1444, 1521]
Type: List(NonNegativeInteger)
sage.sh is:
==============================
#!/bin/sh
base=$(basename $1 .tmp)
cat $1 | sage -q | sed 's/^sage: //' > $base.out
==============================
The first parameter that script gets is a filename of the form
rxcXXXXX.tmp where the XXXXX is a random number (random(2^32)).
Clearly, this enables me not only to call Sage, but also Maple,
Mathematica, etc. The question is only whether I do the massaging of the
output of the respective programs (sage, mathematica, maple) in the
respective shell script or via string transformations in FriCAS. For
Sage I have chosen the script, because string handling in FriCAS is
rather limited without having an equivalent to regular expression
handling like in sed/awl/perl.
If I could eliminate the need of accessing the file system that would be
great. I guess, it should be diable if I use specific features from
SBCL, but I would like to have something that works for any lisp that
FriCAS supports.
You do not say what you want from Sage. IMO "proper" way of using
external programs involves appropriate communitation protocol and
ways to translate data.
Proper way is more involved. What I want is just to give our users
something at hand that enables them to build on top of a simple
communication protocol (like my RunExternalCommand) and do the remaining
"interpret(parse make_output_fricas_like(external_output)" in an ad hoc
fashion.
This might or might not be the start of a true interface to external CAS
(not really my goal), but I just wanted to call two functions from Sage
and use their output for comparison/testing of my code.
OTOH I am not sure if there are good ways to get information out of
Sage.
In general, you are right. But I do not want to write an interface to
everything in Sage. That would be equally difficult as the interface to
FriCAS in Sage. It also only covers a part of FriCAS.
Recently, Kurt told me about his experiments.
https://github.com/nilqed/spadlib/blob/master/pipe/src/pipe.spad
I haven't yet experimented with it, but it looks interesting.
I might probably use that for myself, but this code seems to be bound to
SBCL and not easily doable if the underlying lisp can be any other of
the FriCAS supported lisps.
Ralf
--
You received this message because you are subscribed to the Google Groups "FriCAS -
computer algebra system" group.
To unsubscribe from this group and stop receiving emails from it, send an email
to [email protected].
To view this discussion on the web visit
https://groups.google.com/d/msgid/fricas-devel/e3406730-bb70-d19b-e99f-fabea71b8c9e%40hemmecke.org.
-------------------------------------------------------------------
---
--- FriCAS Run External Command
--- Copyright (C) 2023 Ralf Hemmecke <[email protected]>
---
-------------------------------------------------------------------
-- This program is free software: you can redistribute it and/or modify
-- it under the terms of the GNU General Public License as published by
-- the Free Software Foundation, either version 3 of the License, or
-- (at your option) any later version.
--
-- This program is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-- GNU General Public License for more details.
--
-- You should have received a copy of the GNU General Public License
-- along with this program. If not, see <http://www.gnu.org/licenses/>.
-------------------------------------------------------------------
)if LiterateDoc
\documentclass{article}
\usepackage{qeta}
\begin{document}
\title{Interface to external programs}
\author{Ralf Hemmecke}
\date{11-Apr-2023}
\maketitle
\begin{abstract}
The package \qetatype{RunExternalCommand} implements a way to call
an external program with parameters. The lines of output of the
program (stdout) is captured into a list of strings.
\end{abstract}
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\tableofcontents
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\section{Introduction}
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
Since FriCAS is based on Lisp and there is no generic way for
different flavours of Lisp to call and capture the output of external
programs, we use \code{systemCommand} form
\spadtype{MoreSystemCommands} in order to call a program that is
assumed to take a filename as its first argument in order to write the
output to this file instead of stdout.
%
Eventually, the lines of this (temporary) file are read back into
FriCAS and the file is deleted.
The name of the temporary file is generated by the
\qetafun{run}{RunExternalCommand} function in a way that allows
running several FriCAS processes in parallel without conflicting
temporary files.
How to interpret the resulting strings as proper FriCAS object is not
part of this low-level interface.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\section{Implementation}
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\subsection{Package XSageMath}
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
)endif
)abbrev package RUNXCMD RunExternalCommand
++ RunExternalCommand provides an interface to run an external command
++ and capture its standard output.
RunExternalCommand: with
setExecutable: (String, String) -> String
++ setExecutable(prog, cmd) stores the commandline cmd under the
++ key/name prog. It is not checked whether cmd is reachable via
++ the current PATH, so it is advisable to set the full path to
++ the executable together with its standard options.
knownCommands: () -> List List String
++ knownCommands() returns a list of all program names together
++ with their respective commandline values.
run: (String, String) -> List String
++ run(prog, cmd) returns run(prog, cmd, []).
run: (String, String, List String) -> List String
++ run(prog, cmd, args) creates a temporary filename
++ rxcXXXXX.tmp where XXXXX is a random number. If cmd is not
++ empty, it writes cmd to rxcXXXXX.tmp and gives this as the
++ first argument to the prog followed by the arguments args.
++ If cmd is empty
++ The prog is assumed to read the temporary file and write its
++ output into a file with the same name but extension .out
++ instead of .tmp. The temporary files are assumed to be
++ created in the current directory and removed after execution.
++ prog is considered to be a key in previously stored
++ executables hash table via setExecutable. If the table has no
++ such key, then prog is taken literally as the name of a
++ program.
== add
executables: XHashTable(String, String) := table() -- package global
setExecutable(prog: String, cmd: String): String ==
s: String := elt(executables, prog, "")
executables.prog := cmd
return s
knownCommands(): List List String ==
[[prog, executables.prog] for prog in keys executables]
-- local
readFile(fn: FileName): List String ==
not exists? fn => empty()
f: TextFile := open(fn, "input")
lines: List String := empty()
while not endOfFile? f repeat lines := cons(readLine! f, lines)
close! f
return reverse! lines
-- local
join(sep: String, args: List String): String ==
empty? args => empty()
lensep: NonNegativeInteger := #sep
len: NonNegativeInteger := #(first args)
for a in rest args repeat len := len + lensep + #a
str: String := new(len, space())
i := minIndex str
a := first args
for j in 1..#a repeat (qsetelt!(str, i, qelt(a, j)); i:=i+1)
for arg in rest args repeat
for j in 1..lensep repeat (qsetelt!(str, i, qelt(sep, j)); i:=i+1)
for j in 1..#arg repeat (qsetelt!(str, i, qelt(arg, j)); i:=i+1)
str
-- local
mktemp(dir: String, basename: String): String ==
for i in 0..99 repeat -- return for non-existing file
base: String := concat(basename, string random(2^31))
fn: FileName := filename(dir, base, "out")
not exists? fn => return base
-- We should never come here.
error "could not find temporary filename"
run(prog: String, cmd: String, args: List String): List String ==
base: String := mktemp("", "rxc")
fn: FileName := filename("", base, "tmp")
tmpfilename: String := fn :: String
tf: TextFile := open(fn, "output")$TextFile
writeLine!(tf, cmd)
close! tf
exe: String := elt(executables, prog, prog)
cmdparts: List String := concat([exe, tmpfilename], args)
cmd: String := join(" ", cons("system", cmdparts))
systemCommand(cmd)$MoreSystemCommands
fn: FileName := filename("", base, "out")
lines: List String := readFile fn
systemCommand(concat ["system rm ", base, ".*"])$MoreSystemCommands
lines
run(prog: String, cmd: String): List String == run(prog, cmd, [])
)if LiterateDoc
\end{document}
)endif