{"id":19080410,"url":"https://github.com/seeminglyscience/scriptblockdisassembler","last_synced_at":"2025-04-30T06:11:08.460Z","repository":{"id":45342887,"uuid":"428750858","full_name":"SeeminglyScience/ScriptBlockDisassembler","owner":"SeeminglyScience","description":"See generated code for a ScriptBlock","archived":false,"fork":false,"pushed_at":"2021-12-19T21:28:15.000Z","size":26,"stargazers_count":18,"open_issues_count":2,"forks_count":2,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-03-30T13:22:33.159Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/SeeminglyScience.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2021-11-16T17:24:46.000Z","updated_at":"2025-03-12T08:16:27.000Z","dependencies_parsed_at":"2022-08-20T08:40:23.453Z","dependency_job_id":null,"html_url":"https://github.com/SeeminglyScience/ScriptBlockDisassembler","commit_stats":null,"previous_names":[],"tags_count":1,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SeeminglyScience%2FScriptBlockDisassembler","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SeeminglyScience%2FScriptBlockDisassembler/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SeeminglyScience%2FScriptBlockDisassembler/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SeeminglyScience%2FScriptBlockDisassembler/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/SeeminglyScience","download_url":"https://codeload.github.com/SeeminglyScience/ScriptBlockDisassembler/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":251651231,"owners_count":21621716,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-09T02:23:31.747Z","updated_at":"2025-04-30T06:11:08.440Z","avatar_url":"https://github.com/SeeminglyScience.png","language":"C#","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003ch1 align=\"center\"\u003eScriptBlockDisassembler\u003c/h1\u003e\n\n\u003cp align=\"center\"\u003e\n    \u003csub\u003e\n        Show a pseudo-C# representation of the code that the PowerShell compiler generates for a given ScriptBlock.\n    \u003c/sub\u003e\n    \u003cbr /\u003e\u003cbr /\u003e\n    \u003ca title=\"Commits\" href=\"https://github.com/SeeminglyScience/ScriptBlockDisassembler/commits/master\"\u003e\n        \u003cimg alt=\"Build Status\" src=\"https://github.com/SeeminglyScience/ScriptBlockDisassembler/workflows/build/badge.svg\" /\u003e\n    \u003c/a\u003e\n    \u003ca title=\"ScriptBlockDisassembler on PowerShell Gallery\" href=\"https://www.powershellgallery.com/packages/ScriptBlockDisassembler\"\u003e\n        \u003cimg alt=\"PowerShell Gallery Version (including pre-releases)\" src=\"https://img.shields.io/powershellgallery/v/ScriptBlockDisassembler?include_prereleases\u0026label=gallery\"\u003e\n    \u003c/a\u003e\n    \u003ca title=\"LICENSE\" href=\"https://github.com/SeeminglyScience/ScriptBlockDisassembler/blob/master/LICENSE\"\u003e\n         \u003cimg alt=\"GitHub\" src=\"https://img.shields.io/github/license/SeeminglyScience/ScriptBlockDisassembler\"\u003e\n    \u003c/a\u003e\n\u003c/p\u003e\n\n## Install\n\n```powershell\nInstall-Module ScriptBlockDisassembler -Scope CurrentUser -Force\n```\n\n## Why\n\nEver try to read [`Compiler.cs`][compiler] in [PowerShell/PowerShell][powershell]? It's doable, but tedious. Especially for more complex issues it'd be nice to just get a readable version of the final expression tree.\n\nSo I wrote this. It forces the `ScriptBlock` to be compiled, and then digs into it with reflection to find the LINQ expression tree it generated. Then runs it through [ReadableExpressions][readable] with some PowerShell specific customizations and boom we got something much easier to understand.\n\nYou may want this if:\n\n1. You're working on the compiler\n2. You're just curious how certain PowerShell code is compiled\n\n## Demo\n\n```powershell\n{ $a = 10 } | Get-ScriptBlockDisassembly\n```\n\n```csharp\n// ScriptBlock.EndBlock\n(FunctionContext funcContext) =\u003e\n{\n    ExecutionContext context;\n    try\n    {\n        context = funcContext._executionContext;\n        MutableTuple\u003cobject, object[], object, object, PSScriptCmdlet, PSBoundParametersDictionary, InvocationInfo, string, string, object, LanguagePrimitives.Null, LanguagePrimitives.Null, LanguagePrimitives.Null, LanguagePrimitives.Null, LanguagePrimitives.Null, LanguagePrimitives.Null\u003e locals = (MutableTuple\u003cobject, object[], object, object, PSScriptCmdlet, PSBoundParametersDictionary, InvocationInfo, string, string, object, LanguagePrimitives.Null, LanguagePrimitives.Null, LanguagePrimitives.Null, LanguagePrimitives.Null, LanguagePrimitives.Null, LanguagePrimitives.Null\u003e)funcContext._localsTuple;\n        funcContext._functionName = \"\u003cScriptBlock\u003e\";\n        funcContext._currentSequencePointIndex = 0;\n        context._debugger.EnterScriptFunction(funcContext);\n        try\n        {\n            funcContext._currentSequencePointIndex = 1;\n\n            if (context._debuggingMode \u003e 0)\n            {\n                context._debugger.OnSequencePointHit(funcContext);\n            }\n\n            // Note, this here is the actual $a = 10\n            locals.Item009 = 10;\n            context.QuestionMarkVariableValue = true;\n        }\n        catch (FlowControlException)\n        {\n            throw;\n        }\n        catch (Exception exception)\n        {\n            ExceptionHandlingOps.CheckActionPreference(funcContext, exception);\n        }\n        funcContext._currentSequencePointIndex = 2;\n\n        if (context._debuggingMode \u003e 0)\n        {\n            context._debugger.OnSequencePointHit(funcContext);\n        }\n\n    }\n    finally\n    {\n        context._debugger.ExitScriptFunction();\n    }\n}\n```\n\n## How do I read it?\n\nEach named block has it's own delegate (of type `Action\u003cFunctionContext\u003e`) that will be disassembled. So at minimum\neach block will look like this:\n\n```csharp\n// ScriptBlock.EndBlock\n(FunctionContext context) =\u003e\n{\n}\n```\n\nAny time you see a static method called from the class `Fake`, that is a representation of something\nnot directly translatable to C#.\n\n### Fake.Dynamic\n\nAny time you see `Fake.Dynamic` you should imagine it as having this signature:\n\n```csharp\nclass Fake\n{\n    public static TDelegate Dynamic\u003cTDelegate\u003e(DynamicMetaObjectBinder binder);\n}\n```\n\nSo a call to `Fake.Dynamic\u003cAction\u003cint\u003e\u003e(PSPipeWriterBinder.Get())(10)` would get a `Action\u003cint\u003e` from\n`Fake.Dynamic` using the `PSPipeWriterBinder` call site binder and then invoke it.\n\nIn reality the call looks roughly more like:\n\n```csharp\nclosure.Constants[0].Target.Invoke(closure.Constants[0], closure.Constants[1], 10);\n```\n\nBut that doesn't really tell you anything helpful without examining the constants directly.\n\n### Fake.Const\n\nThis represents the retrieval of a constant that is embedded in the delegate.\n\n```csharp\nFake.Const\u003cCommandBaseAst\u003e(typeof(CommandExpressionAst), \"10\");\n//             |                      |                    |\n//             |                      |                    -- ToString value\n//             |                      -- Optional runtime type if different than static type\n//             -- What the constant is statically typed as\n```\n\n## What about binders? How can I read those?\n\nI've added the `Format-ExpressionTree` command to help with that. You're currently\non your own as far as obtaining the expression (I recommend using [ImpliedReflection][ImpliedReflection]),\nbut once you've obtained one you can pipe it to that command.\n\n## Should I use this in a production environment?\n\nNo. I don't know why you would, but don't. It relies heavily on implementation detail\nand will certainly break eventually. Maybe even with a minor release.\n\nThis module should only really ever be used interactively for troubleshooting or exploration.\n\nThe code is also far from optimal in a lot of places. Please don't use it as an example of what you should do.\n\n## Can I compile the C#?\n\nNo. The LINQ expression tree that PowerShell generates makes *heavy* use of constant\nexpressions that cannot easily be translated to pure source code. Any time you see\na method call on a class called `Fake`, that's just some psuedo code I put in to\nexpress what is happening.\n\nIt also makes heavy use of dynamic expressions. For these I use the state of the passed\nbinder to recreate an approximation of it's construction.\n\nAlso most of the API's called in the disassemblied result are non-public.\n\nAlso LINQ expression trees let you do things like fit a whole block of statements into a single expression.\n\n## Optimized vs unoptimized\n\nThere are two modes for the compiler, optimized and unoptimized. By default this command will return the optimized version, but the `-Unoptimized` switch can be specified to change that.\n\nHere are some common reasons the compiler will naturally enter the unoptimized mode:\n\n1. Dot sourcing\n2. Static analysis found the use of a `*-Variable` command\n3. Static analysis found the use of *any* debugger command\n4. Static analysis found references to any `AllScope` variables\n\nOptimization mostly affects how access of local variables are generated.\n\n## Why doesn't this work on PowerShell versions older than 7.2\n\nI just didn't see the need and it would require me to make sure all the private fields for every binder are still the same. If you need this please open an issue.\n\n[readable]: https://github.com/agileobjects/ReadableExpressions \"agileobjects/ReadableExpressions\"\n[compiler]: https://github.com/PowerShell/PowerShell/blob/master/src/System.Management.Automation/engine/parser/Compiler.cs \"Compiler.cs\"\n[powershell]: https://github.com/PowerShell/PowerShell \"PowerShell/PowerShell\"\n[ImpliedReflection]: https://github.com/SeeminglyScience/ImpliedReflection \"SeeminglyScience/ImpliedReflection\"\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fseeminglyscience%2Fscriptblockdisassembler","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fseeminglyscience%2Fscriptblockdisassembler","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fseeminglyscience%2Fscriptblockdisassembler/lists"}