{"id":52,"date":"2011-04-13T00:51:25","date_gmt":"2011-04-13T04:51:25","guid":{"rendered":"http:\/\/griffinscs.wordpress.com\/?p=52"},"modified":"2011-04-13T00:51:25","modified_gmt":"2011-04-13T04:51:25","slug":"scriptshell-a-command-line-host-for-powershellironpython","status":"publish","type":"post","link":"https:\/\/brainslug.azurewebsites.net\/?p=52","title":{"rendered":"ScriptShell: a Command Line host for Powershell\/IronPython"},"content":{"rendered":"<p>I wanted to write about a tool project as soon as I could, because custom tooling is something that I enjoy doing.\u00a0 Time and my mortgage tend to enforce that these excursions can be completed in a weekend or less, so this is a simple, yet effective solution for my Windows scripting needs.<\/p>\n<p><strong>Design Goals<\/strong><\/p>\n<ul>\n<li>Support two popular language options, Powershell and Python.<\/li>\n<li>Target the 4.0 Framework.<\/li>\n<li>Tool contains its own references, including scripting runtimes, for optimal redistribution.<\/li>\n<li>Dynamically reference custom libraries.<\/li>\n<\/ul>\n<p><strong>Use Cases<\/strong><\/p>\n<ul>\n<li>Easily distributed unit\/integration\/regression testing of .NET libraries<\/li>\n<li>Generalized .NET 4.0 platform automation, who needs batch files?<\/li>\n<li>Can be set as the default binding for PS1 and PY files, or maintain your Powershell\/ipy bindings and use GRIFFPS and GRIFFPY extensions.<\/li>\n<\/ul>\n<p><strong>Download <a href=\"http:\/\/dl.dropbox.com\/u\/8701079\/ScriptShell.exe\">ScriptShell 1.0.0.0<br \/>\n<\/a> <a href=\"http:\/\/github.com\/jeffgriffin\/Blog\">Source<\/a><\/strong><\/p>\n<p><strong>Usage<\/strong><\/p>\n<pre>Usage: ScriptShell [script-file-list] [-r reference-directory-list]\n                   [-d scripts-directory-list] [-a]\n\nOptions:\n script-file-list\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 Run the specified script file(s) .\n -r reference-directory-list\u00a0\u00a0 Look for reference assemblies in this\/these\n                               location(s).\n -d scripts-directory-list\u00a0\u00a0\u00a0\u00a0 Run scripts from the specified\n                               directory\/directories.\n -a\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 Automated mode.\u00a0 This option will not prompt\n                               for advancement.\u00a0\n                               Note: Console prompts written into script\n                               files will be unaffected.\n\n When specifying script files with the -s and -d options, the specified\n script files or runnable directory contents must be named with the\n following extensions:\n                               *.py or *.griffpy for IronPython scripts\n                               *.ps1 or *.griffps for PowerShell scripts\";<\/pre>\n<p><strong>Motivation<\/strong><\/p>\n<p>I wrote the first iteration of this tool out of frustration.\u00a0 My client was experiencing issues regarding connection stability with a back-end service.\u00a0 Reproducing the issue was problematic, and results varied by machine and locale.\u00a0 It occurred to me that it might be nice to simplify his testing environment, and that of his support staff by giving him something without a UI, that would call directly into my Model code and would always act in a consistent, predictable way, regardless of who ran it.\u00a0 Something similar to a unit\/integration test platform might be nice, except the software requirements would be unwieldy.\u00a0 A Powershell script seemed like a perfect option, until I realized it singularly targeted the 2.0 framework.\u00a0 There were a few documented workarounds for this, but it was pretty apparent by now that I'd save time by writing up a Command Line Powershell host, targeting the 4.0 Framework, with references to my Model code.<\/p>\n<p><strong>Hosting Powershell<\/strong><\/p>\n<p>This was a relatively easy task, and is primarily just the tedium of tying all of the Powershell hooks to the command line.\u00a0 <a href=\"http:\/\/notgartner.wordpress.com\/2008\/02\/23\/how-to-host-the-powershell-runtime\/\">This post<\/a> by Mitch Denney was particularly helpful as a starting point.\u00a0 Since my <a href=\"http:\/\/msdn.microsoft.com\/en-us\/library\/system.management.automation.host.pshost%28v=vs.85%29.aspx\">PSHost<\/a> would need a UI, I needed to inherit <a href=\"http:\/\/msdn.microsoft.com\/en-us\/library\/system.management.automation.host.pshostuserinterface%28v=vs.85%29.aspx\">PSHostUserInterface<\/a> as well.\u00a0 This is where the hooks are set into the command line.<\/p>\n<pre class=\"brush: csharp; \">\n\npublic class ConsolePsHostUserInterface : PSHostUserInterface\n{\n\t\/\/other tedium\n\n\t\/\/wire to Console\n\tpublic override string ReadLine()\n\t{\n\t\treturn Console.ReadLine();\n\t}\n\n\tpublic override System.Security.SecureString ReadLineAsSecureString()\n\t{\n\t\tSecureString secret = new SecureString();\n\t\tConsoleKeyInfo currentKey;\n\t\twhile ((currentKey=Console.ReadKey(true)).Key != ConsoleKey.Enter)\n\t\t{\n\t\t\tif (currentKey.Key == ConsoleKey.Backspace)\n\t\t\t{\n\t\t\t\tif (secret.Length &gt; 0)\n\t\t\t\t{\n\t\t\t\t\tsecret.RemoveAt(secret.Length - 1);\n\t\t\t\t\tConsole.Write(currentKey.KeyChar);\n\t\t\t\t}\n\t\t\t}\n\t\t\telse\n\t\t\t{\n\t\t\t\tsecret.AppendChar(currentKey.KeyChar);\n\t\t\t\tConsole.Write(&quot;*&quot;);\n\t\t\t}\n\t\t}\n\t\tConsole.WriteLine();\n\t\tsecret.MakeReadOnly();\n\t\treturn secret;\n\t}\n\n\tpublic override void Write(string value)\n\t{\n\t\tConsole.Write(value);\n\t}\n}\n\npublic class ConsolePSHost : PSHost\n{\n\t\/\/more boringness\n\n\t\/\/insert my console UI\n\tprivate PSHostUserInterface _ui = new ConsolePsHostUserInterface();\n\tpublic override PSHostUserInterface UI\n\t{\n\t\tget { return _ui; }\n\t}\n}\n\n<\/pre>\n<p>All that's left to do is to add a lazily-instantiated property for the Powershell environment, console exception handling\u00a0 and a nice calling convention for my application code.<\/p>\n<pre class=\"brush: csharp; \">\n\npublic static class PowerShellRunner\n{\n\tprivate static PowerShell _pws = null;\n\tprivate static PowerShell PowerShell\n\t{\n\t\tget\n\t\t{\n\t\t\tif (_pws == null)\n\t\t\t{\n\t\t\t\t_pws = PowerShell.Create();\n                    \t\tConsolePSHost host = new ConsolePSHost();\n                    \t\t_pws.Runspace = RunspaceFactory.CreateRunspace(host);\n                    \t\t_pws.Runspace.Open();\n\t\t\t}\n\t\t\treturn _pws;\n\t\t}\n\t}\n\n\tpublic static void Run(string file)\n\t{\n\t\tPowerShell.AddScript(File.ReadAllText(file));\n\t\tPowerShell.AddCommand(&quot;Out-Default&quot;);\n\t\ttry { PowerShell.Invoke(); }\n\t\tcatch (Exception e)\n\t\t{\n\t\t\tConsole.Write(e.ToString());\n\t\t\tConsole.WriteLine();\n\t\t}\n\t}\n}\n\n<\/pre>\n<p>So I called into something similar to this from a command line application that searches a known subdirectory of the current location for Powershell scripts and it worked like a charm.\u00a0 It's about now that I realize how much I don't like calling into .NET assemblies with Powershell.\u00a0 Cmdlets are a nice shorthand for common operations, but who can remember all of them?\u00a0 Here's a line of script I wrote to expand a log path with an environment variable:<\/p>\n<pre class=\"brush: powershell; \">\n\n$logName = [System.Environment]::ExpandEnvironmentVariables(&quot;%USERPROFILE%\\ScriptShell\\Test1.log&quot;)\n\n<\/pre>\n<p>I forget how to use that crazy bracket-and-double-colon syntax every time, and I end up having to Google it or look it up in another script.\u00a0 Still, this accomplished what I set out to do, and I know there are plenty of Powershell lovers out there.<\/p>\n<p><strong>Hosting IronPython<\/strong><\/p>\n<p>It dawned on me later that not only is it easier to host the Dynamic Language Runtime, but my personal preference swings toward writing a script using Python's syntax.\u00a0 Here's an example of the same action as above, written in Python:<\/p>\n<pre class=\"brush: python; \">\n\nlogName = Environment.ExpandEnvironmentVariables(&quot;%USERPROFILE%\\ScriptShell\\Test1.log&quot;)\n\n<\/pre>\n<p>Here's what it took to get IronPython support:<\/p>\n<pre class=\"brush: csharp; \">\n\npublic static class PythonRunner\n{\n\tprivate static ScriptEngine _engine = null;\n\tprivate static ScriptEngine ScriptEngine\n\t{\n\t\tget\n\t\t{\n\t\t\tif (_engine == null)\n\t\t\t{\n\t\t\t\t_engine = IronPython.Hosting.Python.CreateEngine();\n\t\t\t\t_engine.Runtime.IO.SetOutput(Console.OpenStandardOutput(), Console.Out);\n\t\t\t\t_engine.Runtime.IO.SetErrorOutput(Console.OpenStandardOutput(), Console.Out);\n\t\t\t}\n\t\t\treturn _engine;\n\t\t}\n\t}\n\n\tpublic static void Run(string file)\n\t{\n\t\ttry { ScriptEngine.CreateScriptSourceFromFile(file).Execute(); }\n\t\tcatch (Exception e)\n\t\t{\n\t\t\tConsole.Write(e.ToString());\n\t\t\tConsole.WriteLine();\n\t\t}\n\t}\n}\n\n<\/pre>\n<p>...easy as a cliche.\u00a0 Of course, in a sense, what I have done is rewritten ipy.exe, without the interactive mode (this might be a good follow-up), but in the fist pass, the easiest way to distribute this functionality was to directly reference my Model code and pass this out as an EXE to be dropped into the install directory of my client's software.\u00a0 This implementation is a generalized implementation of the same concept, where any reference needed by a script is satisfied dynamically with a command line option at run-time.\u00a0 Developers who do not need to redistribute their scripting environment may find that ipy.exe works just fine for them, the Python scripts will look the same in either case.<\/p>\n<p>I had one major hiccup with using the IronPython Runtime for this tool, and it came when I attempted to pull the application out of my Release directory and only use the references I had embedded as resources.\u00a0 The issue and workaround are adequately described <a href=\"http:\/\/ironpython.codeplex.com\/workitem\/21740\">here<\/a>.\u00a0\u00a0 The workaround involves a modification, and custom build of the runtime.\u00a0 Thanks to <a id=\"PostedByLink1\" href=\"http:\/\/www.codeplex.com\/site\/users\/view\/rodrigobarnes\">rodrigobarnes<\/a> for that.<\/p>\n<p><strong>Download <a href=\"http:\/\/dl.dropbox.com\/u\/8701079\/ScriptShell.exe\">ScriptShell 1.0.0.0<br \/>\n<\/a><a href=\"http:\/\/github.com\/jeffgriffin\/Blog\">Source<\/a><\/strong><\/p>\n","protected":false},"excerpt":{"rendered":"<p>I wanted to write about a tool project as soon as I could, because custom tooling is something that I enjoy doing.\u00a0 Time and my mortgage tend to enforce that these excursions can be completed in a weekend or less, so this is a simple, yet effective solution for my Windows scripting needs. Design Goals [&hellip;]<\/p>\n","protected":false},"author":3,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":[],"categories":[5,1],"tags":[6,9,14,27,38,40,45,46,51],"_links":{"self":[{"href":"https:\/\/brainslug.azurewebsites.net\/index.php?rest_route=\/wp\/v2\/posts\/52"}],"collection":[{"href":"https:\/\/brainslug.azurewebsites.net\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/brainslug.azurewebsites.net\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/brainslug.azurewebsites.net\/index.php?rest_route=\/wp\/v2\/users\/3"}],"replies":[{"embeddable":true,"href":"https:\/\/brainslug.azurewebsites.net\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=52"}],"version-history":[{"count":0,"href":"https:\/\/brainslug.azurewebsites.net\/index.php?rest_route=\/wp\/v2\/posts\/52\/revisions"}],"wp:attachment":[{"href":"https:\/\/brainslug.azurewebsites.net\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=52"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/brainslug.azurewebsites.net\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=52"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/brainslug.azurewebsites.net\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=52"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}