• Skip to primary navigation
  • Skip to main content

MarkHing.com

Carefully Crafted Content

  • Home
  • Smallworld GIS
  • Software
  • Investing
  • Random Musings
  • Blog
  • About
    • Privacy Policy
    • Terms of Service
    • GNU General Public License
  • Contact Me
  • Show Search
Hide Search

Using Powershell with Magik

Mark Hing · Jan 9, 2020 ·

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.

Smallworld

About Mark

Mark Hing, a Value Investing and Smallworld GIS specialist, created the Automatic Investor software and is the author of "The Pragmatic Investor" book.

(Buy the book now on Amazon.ca)
(Buy the book now on Amazon.com)

As President of Aptus Communications Inc., he builds cutting-edge FinTech applications for individual investors. He has also used his software expertise to architect, develop and implement solutions for IBM, GE, B.C. Hydro, Fortis BC, ComEd and many others.

When not taking photographs or dabbling in music and woodworking, he can be found on the ice playing hockey -- the ultimate sport of sports.

linkedin   Connect with Mark

All views expressed on this site are my own and not those of my employer.

Copyright © 2023 Aptus Communications Inc. All Rights Reserved.