Magik is a terrific language for many tasks, but it’s at a distinct disadvantage to other languages with respect to interacting with the Operating System and Frameworks.
C#, for example, has access to an entire suite of .NET Framework classes and Powershell has tons of built-in Cmdlets that do just about anything you would require.
Magik is comparatively limited.
However it is possible, and quite easy, to invoke Powershell scripts from Magik in order to obtain a number of benefits, such as accessing the wealth of Powershell’s Cmdlets and being able to use .NET Framework classes.
In some cases, you simply want to invoke something and let it run to completion – such as when you’d like to convert files in a folder to PDFs. In other cases you want Powershell to return results to Magik. Either way, the Magic Magik method is system.input_from_command()
This opens the door that reveals a powerful new world to Magik.
So, how can we use this method to do all these wonderful things?
Input_from_command() takes a string argument telling it what to execute and returns a stream (specifically an external_text_input_stream). As long as you format the command properly and read the results, you’ll be able to invoke Powershell scripts and do whatever Powershell can do – in Magik!
cmd << "powershell E:\mhprojects\temp\magik_mirror.ps1 Hello, Beeble, World"
etis << system.input_from_command(cmd).get_line()
Take a look at the code above. It invokes a Powershell script called, magik_mirror.ps1 and supplies a message passed in as an array of words (in Powershell the commas separating the words create an array). It then returns these words as a line using the get_line() method.
Here’s the magik_mirror.ps1 code.
Param([string]$msg)
Function Magik-Mirror ([String[]]$msg)
{
Try
{
if ($msg) {
foreach ($word in $msg)
{
Write-Output $word
}
}
}
Catch {
$ErrorMessage = $_.Exception.Message
$FailedItem = $_.Exception.ItemName
$err_msg = "Error: " + $ErrorMessage
if ($FailedItem) {
$err_msg = $err_msg + ": " + $FailedItem
}
Write-Host $err_msg
Break
}
Finally {
}
}
# *** Entry point to Script ***
Magik-Mirror $msg
This code reads the message and simply passes it back to Magik.
It reads the message by iterating over the array of words passed in as the $msg parameter and returns those words to Magik using Write-Output in line 10. Anything written out by the script is captured by the Magik stream and ultimately returned by system.input_from_command(). You can also use Write-Host (as in line 21) to return data.
Of course the real power of this technique is the fact you can use Powershell CmdLets and .NET framework classes. The example below, sort_files.ps1, demonstrates the use of CmdLets to sort files in a folder by date/time and filter by a filter string.
Param([string]$path, [string]$filter)
Function Sort-Files ([string]$path,[string]$filter)
{
Try
{
$items = Get-ChildItem -Path $path -Filter $filter -ErrorAction Stop | Sort-Object -Property LastWriteTime
if ($items) {
foreach ($item in $items)
{
# if the item is NOT a directory, then process it.
if ($item.Attributes -ne "Directory")
{
Write-Output $item.Name
}
}
}
}
Catch {
$ErrorMessage = $_.Exception.Message
$FailedItem = $_.Exception.ItemName
$err_msg = "Error: " + $ErrorMessage
if ($FailedItem) {
$err_msg = $err_msg + ": " + $FailedItem
}
write-host $err_msg
Break
}
} #end Sort-Files
Sort-Files -path $path -filter $filter
On the Magik side, invoke sort_files.ps1 using the code below.
Magik> s << system.input_from_command("powershell E:\mhprojects\temp\sort_files.ps1 E:\mhprojects\temp *.magik")
a sw:external_text_input_stream
Magik> s.get_line()
"viewport_layout.magik" True
Magik> s.get_line()
"closures.magik" True
Magik> s.get_line()
"PowershellMagik.magik" True
Magik> s.get_line()
unset False
Magik>
We can get the next line from the stream using the get_line() method. get_line() returns unset when there are no more lines to return — as in line 19. You can also use the get_word() method if you want to read words, rather than entire lines, from the stream.
Now we’ve seen how easy it is to invoke Powershell scripts, let’s write a procedure to encapsulate everything we’ve learned.
I’ve created a proc below, but you could also create a method. Of course you should remove the hard-coded values (and it’s best to read configuration information from a config file).
_global invoke_ps <<
_proc (p_cmd_name, _optional p_return_lines?, _gather p_args)
_local l_return << rope.new()
_if p_cmd_name _is _unset _orif p_cmd_name.empty?
_then
write("ERROR: Command Name is missing.")
_return l_return, _false
_endif
_local l_method << :get_word|()|
_if p_return_lines? _is _true
_then
l_method << :get_line|()|
_endif
# get the powershell scripts folder.
_local l_cmd_path << "E:\mhprojects\temp\"
# ensure the folder exists.
_if ~system.file_exists?(l_cmd_path)
_then
write("ERROR: folder ",l_cmd_path," does _NOT exist.")
_return l_return, _false
_endif
_local l_ps_name << "powershell "
_local l_ps_ext << ".ps1 "
_local l_params << ""
_try _with cond
# format the parameters.
_for a_param _over p_args.fast_elements()
_loop
l_params << write_string(l_params," ",a_param).trim_spaces()
_endloop
_local l_cmd_to_exec << write_string(l_ps_name, l_cmd_path, p_cmd_name, l_ps_ext, l_params)
# execute the powershell Cmdlet.
_local l_etis << system.input_from_command(l_cmd_to_exec)
_loop
_if (ret_val << l_method.send_to(l_etis)) _is _unset
_then
_leave
_endif
l_return.add_last(ret_val)
_endloop
l_etis.close()
_when error
write("ERROR: ",cond.report_string)
_return l_return, _false
_endtry
_return l_return, _true
_endproc
$
To invoke the sort_files.ps1 script, via this procedure, do the following.
Magik> r << invoke_ps("sort_files",_true,"E:\mhprojects\temp *.magik")
sw:rope:[1-3]
Magik> print(r)
rope(1,3):
1 "viewport_layout.magik"
2 "closures.magik"
3 "PowershellMagik.magik"
Magik>
Note the second argument in line 1 is set to _true. This tells the proc we want to return lines rather than, the default, individual words.
So now Magik has access to all the Powershell CmdLets, what’s next?
How about .NET Framework classes?
Since Powershell scripts can access .NET, it’s a simple matter to allow Magik to have that same access. Take a look at the code below.
Add-Type -AssemblyName System.ServiceProcess
Function Get-Processes ()
{
Try
{
[diagnostics.process]::GetProcesses()
}
Catch {
$ErrorMessage = $_.Exception.Message
$FailedItem = $_.Exception.ItemName
$err_msg = "Error: " + $ErrorMessage
if ($FailedItem) {
$err_msg = $err_msg + ": " + $FailedItem
}
Write-Host $err_msg
Break
}
}
# *** Entry point to Script ***
Get-Processes
In line 1 we include the assembly using the Add-Type cmdlet. Then we call the GetProcesses() method, in line 7, which returns a list of processes (i.e. writes them out in string format). It’s basically a two-line script.
When we invoke it with our new proc, we get the following results.
Magik> r << invoke_ps("get_processes", _true)
sw:rope:[1-115]
Magik> print(r)
rope(1,115):
1 ""
2 "Handles NPM(K) PM(K) WS(K) CPU(s) Id SI ProcessName "
3 "------- ------ ----- ----- ------ -- -- ----------- "
4 " 913 50 37040 85516 15.16 13984 2 Code "
5 " 309 28 5852 18792 0.50 2752 2 taskhostw "
6 " 529 125 9780 24240 2948 0 vmms "
7 " 548 28 12996 48516 0.55 13388 2 SearchUI "
8 " 101 9 3920 9380 0.67 16932 2 conhost "
9 " 206 16 12864 22384 0.27 15648 2 TortoiseHgOverlayServer"
10 " 816 129 69432 90160 972 0 svchost "
11 " 94 7 1508 6648 1956 0 vmacthlp "
12 " 349 10 2000 8544 1468 0 WUDFHost "
13 " 448 30 33324 21872 2740 0 HealthService "
14 " 85 8 2636 7332 0.02 15176 2 conhost "
15 " 117 8 1816 4152 768 1 csrss "
16 " 32 5 83372 84732 29.08 16984 2 method_finder "
17 " 54 2 408 1260 568 0 smss "
18 " 479 36 14700 22340 1416 0 svchost "
19 " 36 4 1568 2740 0.00 15732 2 cmd "
20 " 357 73 35904 42172 0.47 14352 2 SCNotification "
...
Magik>
And just like that, Magik can see current processes running on the local computer (if you want to see all lines, set !print_length! appropriately before printing). Of course you can use pretty much any .NET functionality that returns string data.
The myriad uses of this technique are virtually limitless. You can use it so Magik can access REST endpoints via HTTP/HTTPS, control OS functionality, interface with hardware and do tons of other things.
Use your imagination. Expand Magik’s capabilities. Easily reuse existing functionality. system.input_from_command() truly opens doors and transforms Smallworld into a little bit Biggerworld.