{"id":1323,"date":"2012-04-23T00:00:00","date_gmt":"2012-04-23T00:00:00","guid":{"rendered":"https:\/\/test.simple-talk.com\/uncategorized\/practical-powershell-pruning-file-trees-and-extending-cmdlets\/"},"modified":"2021-05-17T18:36:15","modified_gmt":"2021-05-17T18:36:15","slug":"practical-powershell-pruning-file-trees-and-extending-cmdlets","status":"publish","type":"post","link":"https:\/\/www.red-gate.com\/simple-talk\/development\/dotnet-development\/practical-powershell-pruning-file-trees-and-extending-cmdlets\/","title":{"rendered":"Practical PowerShell: Pruning File Trees and Extending Cmdlets"},"content":{"rendered":"<div id=\"pretty\">\n<h2 id=\"contents\">Contents<\/h2>\n<ul>\n<li><a href=\"#contents\">Practical PowerShell: Pruning File Trees<\/a>\n<ul>\n<li><a href=\"#revealing\">Revealing the Problem<\/a><\/li>\n<li><a href=\"#pruning\">What Tree Pruning Can Do For You<\/a><\/li>\n<li><a href=\"#customizations\">Adding your own Customizations to Get-ChildItem<\/a>\n<ul>\n<li><a href=\"#example1\">Example 1: FilterContainersOnly<\/a><\/li>\n<li><a href=\"#example2\">Example 2: FilterSvn<\/a><\/li>\n<li><a href=\"#example3\">Example 3: FilterExcludeTree<\/a><\/li>\n<\/ul>\n<\/li>\n<li><a href=\"#infrastructure\">Infrastructure for Customizing Get-EnhancedChildItem<\/a>\n<ul>\n<li><a href=\"#step1\">Step 1: Hooking up your filter<\/a><\/li>\n<li><a href=\"#step2\">Step 2: Add your parameter(s) to the $introducedParameters list.<\/a><\/li>\n<li><a href=\"#step3\">Step 3: Add your parameter(s) to the cmdlet signature<\/a><\/li>\n<li><a href=\"#step4\">Step 4: Document your added functionality<\/a><\/li>\n<\/ul>\n<\/li>\n<li><a href=\"#customizinganycmdlet\">Customizing Any Cmdlet<\/a><\/li>\n<li><a href=\"#conclusion\">Conclusion<\/a><\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<p class=\"start\">The <strong>Get-ChildItem<\/strong> cmdlet is arguably the fundamental workhorse of PowerShell. Get-ChildItem is analogous to ls in Unix\/Linux or dir in DOS in that it allows you to view the contents of your filesystem. The documentation summary simply states &#8220;Gets the items and child items in one or more specified locations.&#8221; There is a reason that it does not say <em>files<\/em> and <em>folders<\/em><em>: <\/em>In PowerShell, locations <em>may be<\/em> folders, and items <em>may be<\/em> files, but unlike Linux or DOS, a location in PowerShell may also be in your registry, GAC, variables, environment or elsewhere. But <em>that<\/em> extraordinary design capability is the basis of another story.<\/p>\n<p>Get-ChildItem provides options that let you clamber recursively through a directory tree, include or exclude system items, or filter your output with included or excluded targets. These options provide the flexibility to get a lot out of this cmdlet, even though they seem few in number. At the time of writing, PowerShell V3 is nearing release. This version adds a few <a href=\"http:\/\/social.technet.microsoft.com\/wiki\/contents\/articles\/4788.powershell-v3-tips-and-tricks-what-s-new-in-v3-en-us.aspx\">new parameters<\/a>: -Directory to exclude files; -File to exclude directories; -Attributes to filter by selected attributes; -Hidden, -ReadOnly, and -System to filter by those particular attributes. But for me, Get-ChildItem still seemed to be missing some crucial functionality. My wish-list included being able to filter out those files that were not under source control, which is a feature I use extensively in my series of articles on Subversion. Most importantly, I need to be able to exclude entire subtrees, hence the topic of this article.<\/p>\n<p>With this article I am going to reverse my usual approach by starting with the practical application and then work backwards towards the theory. This provides you what you need to be immediately productive, so that you can then stop reading as soon as you have run out of time or interest in understanding everything behind it.<\/p>\n<p>I&#8217;ll start by revealing the not very surprising ending: the new cmdlet I&#8217;ve written, <strong>Get-EnhancedChildItem<\/strong>, extends the capabilities of Get-ChildItem to include, among others, the -ExcludeTree parameter. Please refer to the <a href=\"http:\/\/cleancode.sourceforge.net\/api\/powershell\/CleanCode\/FileTools\/Get-EnhancedChildItem.html\">Get-EnhancedChildItem API<\/a> for the details of how to use it, and use the <a href=\"http:\/\/cleancode.sourceforge.net\/wwwdoc\/download.html\">download<\/a> page to get the code.<\/p>\n<h2 id=\"revealing\">Revealing the Problem<\/h2>\n<p>Consider first excluding <em>files<\/em> from a directory listing. By using the standard -Exclude parameter with an argument of <strong>*.user<\/strong><strong>, <\/strong>you can filter out two files in this example; the left side shows the unfiltered result and highlights the items that are filtered out on the right side when the -Exclude parameter is applied. Also, the output shown here is a pretty-printed version of output from Get-ChildItem that illustrates the relevant points more clearly than the raw output of the cmdlet. These examples also assume the use of the -Recurse parameter to grab all files and directories under the current directory (click for enlarged image).<\/p>\n<p class=\"illustration\"><a href=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/imported\/1477-image1.jpg\"><img decoding=\"async\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/imported\/1477-image1small.jpg\" alt=\"1477-image1small.jpg\" \/><\/a><\/p>\n<p>Now consider what happens when applying a similar filter to exclude a directory, e.g. -Exclude bin:<\/p>\n<p class=\"illustration\"><a href=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/imported\/1477-image2.jpg\"><img decoding=\"async\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/imported\/1477-image2small.jpg\" alt=\"1477-image2small.jpg\" \/><\/a><\/p>\n<p>The subdirectories named <strong>bin<\/strong> are filtered out <em>but their children are not<\/em>! And therein lies both the problem and the window of opportunity for the new -ExcludeTree parameter.<\/p>\n<h2 id=\"pruning\">What Tree Pruning Can Do For You<\/h2>\n<p>With Get-EnhancedChildItem, you can substitute -ExcludeTree bin for -Exclude bin to yield the filtering that most of us would expect and prefer: where the <em>entire<\/em> subtree rooted at each <strong>bin<\/strong> instance is filtered out.<\/p>\n<p class=\"illustration\"><a href=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/imported\/1477-image3.jpg\"><img decoding=\"async\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/imported\/1477-image3small.jpg\" alt=\"1477-image3small.jpg\" \/><\/a><\/p>\n<p>Also, just like Exclude, ExcludeTree accepts wildcards so that, if you used <strong>bin*<\/strong> instead of <strong>bin<\/strong><strong>,<\/strong> that would prune not only instances of <strong>bin<\/strong> but also <strong>bin-backup<\/strong>, <strong>bin2<\/strong>, etc.<\/p>\n<p>I&#8217;d like to mention up-front that Get-EnhancedChildItem works in both PowerShell V2 and V3 but-in its current implementation-it works <em>better<\/em> in V2 than V3. Why? As mentioned earlier, PowerShell V3 introduced several new parameters for Get-ChildItem that are specific to the <strong>FileSystem<\/strong> provider. To have provider-specific parameters requires making them <em>dynamic parameters<\/em>. And dynamic parameters are <em>not<\/em> exposed by the meta-programming utility used to generate the extensible base script for Get-ChildItem. Thus, in PowerShell V3, Get-EnhancedChildItem is <em>not<\/em> a superset of Get-ChildItem because it does not support the new, dynamic parameters. (For more on dynamic parameters see the eponymous section on the PowerShell <a href=\"http:\/\/technet.microsoft.com\/en-us\/library\/dd347600.aspx\">about_Functions_Advanced_Parameters<\/a> help page.)<\/p>\n<h2 id=\"customizations\">Adding your own Customizations to Get-ChildItem<\/h2>\n<p>With a short look behind the scenes at Get-EnhancedChildItem you will quickly be able to add customizations of your own. Get-EnhancedChildItem calls Get-ChildItem, passing through any standard parameters you specify, and then pipes the output through additional dynamic filters required by any of its custom parameters you specify. Consider this command:<\/p>\n<pre class=\"lang:c# theme:vs2012\">Get-EnhancedChildItem -Recurse -Force -Svn\r\n-Exclude *.txt -ExcludeTree doc*,man -FullName -Verbose <\/pre>\n<p>That invocation includes several Get-ChildItem standard parameters (Recurse, Force, Exclude), several Get-EnhancedChildItem custom parameters (Svn, ExcludeTree, FullName), and one common parameter (Verbose). Here are the first few lines of output-present only because the Verbose switch was included:<\/p>\n<pre class=\"lang:c# theme:vs2012\">VERBOSE: [[ Get-ChildItem @PSBoundParameters | FilterExcludeTree | FilterSvn | FilterFullName ]]\r\nVERBOSE: Parameters to Get-ChildItem = [[\r\nVERBOSE:\u00a0\u00a0\u00a0\u00a0\u00a0Recurse = True\r\nVERBOSE:\u00a0\u00a0\u00a0\u00a0\u00a0Force = True\r\nVERBOSE:\u00a0\u00a0\u00a0\u00a0\u00a0Exclude = *.txt\r\nVERBOSE:\u00a0\u00a0\u00a0\u00a0\u00a0Verbose = True\r\nVERBOSE: ]] <\/pre>\n<p>The first line shows the dynamic code generated from the Get-EnhancedChildItem invocation; it always begins with a call to the underlying Get-ChildItem cmdlet and the output is piped to filters appropriate to the parameters you specify on the command line. The remaining lines above show the parameters that are passed through to the Get-ChildItem cmdlet (via the PSBoundParameters variable).<\/p>\n<p>Each of the custom parameters (Svn, ExcludeTree, FullName) adds a filter to the output chain, as shown above. Thus, to add your own custom behavior your main task is writing an appropriate filter. The key to this is to realize that the output stream from Get-ChildItem consists of a collection of System.IO.DirectoryInfo and System.IO.FileInfo objects (in the context of the <strong>FileSystem<\/strong> provider). So any downstream filters may make use of properties of these objects.<\/p>\n<h3 id=\"example1\">Example 1: FilterContainersOnly<\/h3>\n<p>This is one of the simplest possible of filters, and corresponds to the -ContainersOnly switch. (This switch is not used in the prior example but its simplicity makes it is a good place to start.)<\/p>\n<pre class=\"lang:c# theme:vs2012\">filter FilterContainersOnly()\r\n{\r\n\u00a0\u00a0if ($_.PSIsContainer) { $_ }\r\n}\r\n<\/pre>\n<p>In this case, the filter examines the PSIsContainer property. If it is true-indicating the object is a directory-the object is passed through, otherwise nothing is emitted and the object does not propagate any further.<\/p>\n<h3 id=\"example2\">Example 2: FilterSvn<\/h3>\n<p>A bit more complicated filter, this corresponds to the -Svn switch, letting you filter out any files and folders that are not under Subversion source control.<\/p>\n<pre class=\"lang:c# theme:vs2012\">filter FilterSvn()\r\n{\r\n\u00a0\u00a0$svnStatus = (svn status --verbose --depth empty $_.FullName 2&gt;&amp;1)\r\n\u00a0\u00a0$svnFilter = ($svnFilter -notmatch \"^[?I]|is not a working copy\")\r\n\u00a0\u00a0if ($svnFilter) { $_ }\r\n} <\/pre>\n<p>The first line invokes the <strong>svn status<\/strong> command on the current file or folder, embodied in the FullName property of either DirectoryInfo or FileInfo objects. The depth parameter specifies to check only the current item, not its descendants. The verbose parameter forces it to report even if the item is up-to-date. Finally, the stderr stream is wrapped into the stdout stream in preparation for validating in the next line.<\/p>\n<p>The second line runs a regular expression comparison to see if the status returned indicates the file is not a Subversion-controlled item. The <strong>svn status<\/strong> command returns a status line beginning with a question mark (?) for <em>unversioned<\/em> files or the letter &#8220;I&#8221; for <em>ignored<\/em> files, both of which constitute non-Subversion-controlled files. Additionally, because the filter is repeatedly invoked on every item returned by the base Get-ChildItem-including non-versioned descendants of non-versioned folders-<strong>svn status<\/strong> fails on such items, returning a warning of the form &#8220;svn: warning: W155007: &#8216;<em>filename<\/em>&#8216; is not a working copy&#8221;. Those children must be considered non-versioned as well, of course; hence, the latter part of the regular expression in the filter above.<\/p>\n<p>The final line uses the results of the previous comparison as a gate to emit or to suppress the current object downstream.<\/p>\n<h3 id=\"example3\">Example 3: FilterExcludeTree<\/h3>\n<p>This filter corresponds to the -ExcludeTree parameter, the main focus of this article.<\/p>\n<pre class=\"lang:c# theme:vs2012\">filter FilterExcludeTree()\r\n{\r\n\u00a0\u00a0$target = $_\r\n\u00a0\u00a0Coalesce-Args $Path \".\" | % {\r\n\u00a0\u00a0\u00a0\u00a0$canonicalPath = (Get-Item $_).FullName\r\n\u00a0\u00a0\u00a0\u00a0if ($target.FullName.StartsWith($canonicalPath)) {\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$pathParts = $target.FullName.substring($canonicalPath.Length + 1).split(\"\\\");\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0if ( ! ($excludeList | where { $pathParts -like $_ } ) ) { $target }\r\n\u00a0\u00a0\u00a0\u00a0}\r\n\u00a0\u00a0}\r\n} <\/pre>\n<p>This filter, unlike the previous two, uses external data-the script-level $excludeList variable defined in the preamble of the Get-EnhancedChildItem code to contain the value of the ExcludeTree parameter. It also uses a supplementary function, <strong>Coalesce-Args<\/strong> (found in <a href=\"http:\/\/solutionizing.net\/2008\/12\/20\/powershell-coalesce-and-powershellasp-query-string-parameters\/\">this post<\/a> by Keith Dahlby), which merely sends the current directory down the pipe in the event that the invocation did not specify any paths.<\/p>\n<p>This code begins by remembering the current file or folder under investigation (remember that the goal of the filter is to determine whether the current item should be excluded as per your specification of the ExcludeTree parameter) because subsequent lines reuse the $_ automatic variable.<\/p>\n<p>Next, the code identifies which of those paths that you specified to trawl is an ancestor of the current item, because you can specify multiple paths rather than just a single path, as in Get-EnhancedChildItem doc,xml,man . . . Once it finds the ancestral path, the two innermost lines make the judgment of whether the path to the current item should be excluded or not. This decision is complicated by the fact that it is possible that some component of the path higher up may have the same name as a component you want to exclude, <em>but those higher components need to be ignored<\/em>. An illustration will make this much clearer. Assume this path exists (notice that it has two path components of the same name, highlighted in red):<\/p>\n<p>\\usr\\testdir\\subdir2\\subdir2-child\\subdir2-grandchild\\subdir2\\doc<\/p>\n<p>Now consider this sequence:<\/p>\n<pre class=\"lang:c# theme:vs2012\">cd \\usr\\testdir\\subdir2\\subdir2-child\r\nGet-EnhancedChildItem . -ExcludeTree subdir2 <\/pre>\n<p>The typical PowerShell algorithm I found for pruning examines the path looking for the exclusion target (subdir2) <em>anywhere<\/em> in the path. But because your current directory has an ancestor of the same name (subdir2), then <em>every<\/em> file in this subtree has subdir2 in its path and hence <em>all<\/em> files will be excluded (pruned). I call this the <em>ancestor trap<\/em>. Consider the suggested algorithms in <a href=\"http:\/\/stackoverflow.com\/questions\/8024103\/how-to-retrieve-a-recursive-Directory-and-File-list-from-powershell-excluding-so\">this StackOverflow post<\/a>. The author ajk provides this succinct code (substitute your directory name to prune for the ExcludeDir placeholder):<\/p>\n<pre class=\"lang:c# theme:vs2012\">Get-ChildItem -Recurse |\r\n? { $_.FullName -notmatch '\\\\ExcludeDir($|\\\\)' } <\/pre>\n<p>That code is clean and elegant. It can even be manipulated to support multiple prune targets by using standard regex notation, e.g. use (ExcludeDir1|ExcludeDir2) in place of ExcludeDir. But it falls prey to the ancestor trap if ExcludeDir happens to occur higher up the path.<\/p>\n<p>The <strong>FilterExcludeTree<\/strong> code used in Get-EnhancedChildItem avoids the ancestor trap by stripping the prefix of the current item above the current directory before evaluating it. Here&#8217;s the code from <strong>FilterExcludeTree<\/strong> refactored so that you can exercise it in isolation:<\/p>\n<pre class=\"lang:c# theme:vs2012\">$excludeList = @(\"stuff\",\"bin\",\"obj*\")\r\nGet-ChildItem -Recurse | % {\r\n\u00a0\u00a0\u00a0\u00a0$pathParts = $_.FullName.substring($pwd.path.Length + 1).split(\"\\\");\r\n\u00a0\u00a0\u00a0\u00a0if ( ! ($excludeList | where { $pathParts -like $_ } ) ) { $_ }\r\n} | \r\n% { $_.fullname } # use this last pipe just to get more concise output <\/pre>\n<p>The algorithm posted by Keith Hill in the same <a href=\"http:\/\/stackoverflow.com\/questions\/8024103\/how-to-retrieve-a-recursive-Directory-and-list-from-powershell-excluding-so\">StackOverflow post<\/a> works well (once it incorporates a bug fix shown below) and also avoids the ancestor trap. Also, it is more efficient than either my approach or ajk&#8217;s approach in that it stops traversing a subtree when it finds a prune target (so one can allow it the &#8220;deficiency&#8221; of requiring several more lines of code to implement :-). The only drawback-through no fault of its own-is that a recursive algorithm cannot be incorporated into the command chaining strategy I designed into Get-EnhancedChildItem. His code appears below, though I have shortened it slightly by eliminating a redundancy as well as included a bug fix that prevented the algorithm from recursing past 2 levels:<\/p>\n<pre class=\"lang:c# theme:vs2012\">function GetFiles($path = $pwd, [string[]]$exclude)\r\n{ \r\n\u00a0\u00a0\u00a0\u00a0foreach ($item in Get-ChildItem $path)\r\n\u00a0\u00a0\u00a0\u00a0{\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0if ($exclude | Where {$item -like $_}) { continue }\r\n\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0$item \r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0if (Test-Path $item.FullName -PathType Container) \r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0{\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0GetFiles $item.FullName $exclude\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0} \r\n\u00a0\u00a0\u00a0\u00a0} \r\n} <\/pre>\n<p>If you want to experiment with any of these algorithms on different test cases, my <a href=\"http:\/\/cleancode.sourceforge.net\/api\/powershell\/CleanCode\/FileTools\/New-FileTree.html\">New-FileTree<\/a> function, also available in my <a href=\"http:\/\/cleancode.sourceforge.net\/wwwdoc\/APIbookshelf.html\">open source library<\/a>, might be useful. It takes a list of files and directories and generates a tree (albeit the files are empty). Here is an example test scenario that lets you experiment with the ancestor trap:<\/p>\n<pre class=\"lang:c# theme:vs2012\">New-FileTree @\"\r\nsample\\stuff\\readme.txt\r\nsample\\stuff\\main\\Form.cs\r\nsample\\stuff\\main\\Lib.cs\r\nsample\\stuff\\main\\program.cs\r\nsample\\stuff\\main\\bin\\Form.obj\r\nsample\\stuff\\main\\bin\\Lib.obj\r\nsample\\stuff\\main\\bin\\Program.exe\r\nsample\\stuff\\main\\new_objs\\NewForm.cs\r\nsample\\stuff\\main\\objs\\NewForm.cs\r\nsample\\stuff\\main\\projects\\doc1.txt\r\nsample\\stuff\\main\\projects\\doc2.txt\r\nsample\\stuff\\main\\projects_A\\doc1.txt\r\nsample\\stuff\\main\\projects_A\\doc2.txt\r\nsample\\stuff\\main\\stuff\\Form.obj\r\nsample\\stuff\\main\\stuff\\Lib.obj\r\nsample\\stuff\\main\\stuff\\root\\readme.txt\r\nsample\\stuff\\main\\stuff\\root\\main\\Form.cs\r\nsample\\stuff\\main\\stuff\\root\\main\\Lib.cs\r\nsample\\stuff\\main\\stuff\\root\\main\\program.cs\r\nsample\\stuff\\main\\stuff\\root\\main\\bin\\Form.obj\r\nsample\\stuff\\main\\stuff\\root\\main\\bin\\Lib.obj\r\nsample\\stuff\\main\\stuff\\root\\main\\bin\\Program.exe\r\nsample\\stuff\\main\\stuff\\root\\main\\new_objs\\NewForm.cs\r\nsample\\stuff\\main\\stuff\\root\\main\\objs\\NewForm.cs\r\nsample\\stuff\\main\\stuff\\root\\main\\stuff\\Form.obj\r\nsample\\stuff\\main\\stuff\\root\\main\\stuff\\Lib.obj\r\n\"@ | % { $_.FullName } <\/pre>\n<p>Notice that this test scenario includes the stuff component up to three distinct places in the path! Once you have built that file tree, then change your current directory to sample\\stuff or sample\\stuff\\main and try to exclude all descendant stuff trees. Here&#8217;s an invocation for Get-EnhancedChildItem, for example:<\/p>\n<pre class=\"lang:c# theme:vs2012\">Get-EnhancedChildItem -Recurse -ExcludeTree stuff -NoContainersOnly -FullName <\/pre>\n<p>I mentioned earlier that Get-ChildItem emits DirectoryInfo and FileInfo objects <em>in the context of the FileSystem provider<\/em>; i.e., <em>if<\/em> <em>and only if<\/em> your current location is in file system space as opposed to environment space or registry space or function space. (If you are in registry space, for example, Get-ChildItem emits RegistryKey objects.) The ExcludeTree parameter depends upon getting DirectoryInfo or FileInfo objects (it needs, among others, the FullName property); hence it is designed to work only with the FileSystem provider. When a parameter only works with a selected subset of providers it should technically be defined as a dynamic parameter rather than a static parameter. That way, if you inadvertently use it with the wrong provider, PowerShell will balk, informing you such a parameter does not exist. (The new Directory, File, Hidden, etc. parameters for Get-ChildItem in PowerShell V3, for example, are defined as dynamic parameters.) Therefore, the ExcludeTree parameter should technically be defined as a dynamic parameter but I chose to make it static mainly because of the <em>discoverability issue<\/em>: it is difficult to discover a dynamic parameter even exists (even Get-Help will not tell you!) unless you read the documentation(!), and one of the elegant features of PowerShell is that it is inherently easy to discover things without the documentation.<\/p>\n<h2 id=\"infrastructure\">Infrastructure for Customizing Get-EnhancedChildItem<\/h2>\n<p>Though creating the filter is the heart of the customization process, there is just a bit of book-keeping work needed to complete adding a customized filter to Get-ChildItem.<\/p>\n<h3 id=\"step1\">Step 1: Hooking up your filter<\/h3>\n<p>The first obvious step is that you must hook up the filter you created. Take a look at the <strong>Connecting Filters<\/strong> region in the Get-EnhancedChildItem code, reproduced here:<\/p>\n<pre class=\"lang:c# theme:vs2012\">#region Connecting Filters\r\nif ($ExcludeTree)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0{ $filters += \"FilterExcludeTree\"\r\n\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\u00a0\u00a0$script:excludeList = $ExcludeTree }\r\n\r\nif ($Svn)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0{ $filters += \"FilterSvn\" }\r\n\r\nif ($ContainersOnly)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0{ $filters += \"FilterContainersOnly\" }\r\nelseif ($NoContainersOnly)\u00a0{ $filters += \"FilterNoContainersOnly\" }\r\n\r\n# These two must be last.\r\n# Thus, must process the -Name parameter here rather than\r\n# pass it through to base cmdlet.\r\nif ($FullName)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0{ $filters += \"FilterFullName\" }\r\nelseif ($Name)\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0{ $filters += \"FilterName\"\r\n\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\u00a0\u00a0[Void]$PSBoundParameters.Remove(\"Name\") }\r\n#endregion Connecting Filters <\/pre>\n<p>Observe that each of my introduced parameters is examined in turn. If the parameter is present, one or more actions are performed. At a minimum, this means adding the filter name to a list of filters. That list is initialized with the base command just before the code region above:<\/p>\n<pre class=\"lang:c# theme:vs2012\">$filters = @(\"$wrappedCmd @PSBoundParameters\") <\/pre>\n<p>And just after the code region, this list of filters is joined into a single command string to execute:<\/p>\n<pre class=\"lang:c# theme:vs2012\">$code = [string]::join(\" | \", $filters) <\/pre>\n<p>Two other lines in the above code region are worthy of note. First, when you specify -ExcludeTree, the code needs to remember its argument; thus, its value is stored in the script-level $excludeList variable that is accessed by the <strong>FilterExcludeTree<\/strong> function, as seen earlier.<\/p>\n<p>Second, notice that the commentary for the last two parameters examined, FullName and Name, indicate they must be last. Generally speaking, the order of examining parameters is irrelevant <em>as long as<\/em> each filter returns the same type of object as output that it received as input, i.e. either a System.IO.DirectoryInfo or System.IO.FileInfo object. But the standard Name parameter of Get-Child-along with the custom FullName parameter of Get-EnhancedChildItem-violate this rule, returning a string object. Thus, if either of these is specified it must be the final filter in the chain. And that is the very reason why I need to handle the Name parameter here instead of passing it through to Get-Child. That is done with two steps: adding the <strong>FilterName<\/strong> function to the command pipeline and removing the Name parameter from the set of passed parameters to inhibit Get-ChildItem from attempting to process it.<\/p>\n<p>I should point out that PowerShell V3, which is not yet released at the time of writing, includes Directory and File parameters that perform the same function as ContainersOnly and NoContainersOnly in Get-EnhancedChildItem. I chose the latter names based on a reference implementation by Andy Schneider (more on this later) and <a href=\"http:\/\/connect.microsoft.com\/PowerShell\/feedback\/details\/308796\/add-enumeration-parameter-to-get-childitem-cmdlet-to-specify-container-non-container-both\">this enhancement request<\/a> on Microsoft Connect.<\/p>\n<h3 id=\"step2\">Step 2: Add your parameter(s) to the $introducedParameters list.<\/h3>\n<p>You will find this variable defined near the top of the script, itemizing each of my custom parameters (i.e. those parameters that are not supported by the underlying Get-ChildItem):<\/p>\n<pre class=\"lang:c# theme:vs2012\">$introducedParameters = `\r\n\u00a0\u00a0\u00a0\u00a0\"ExcludeTree\", `\r\n\u00a0\u00a0\u00a0\u00a0\"Svn\", `\r\n\u00a0\u00a0\u00a0\u00a0\"ContainersOnly\", `\r\n\u00a0\u00a0\u00a0\u00a0\"NoContainersOnly\", `\r\n\u00a0\u00a0\u00a0\u00a0\"FullName\" <\/pre>\n<p>Earlier you saw that the list of parameters passed to Get-ChildItem is a subset of the parameters passed to Get-EnhancedChildItem. The code uses the $introducedParameters list to reduce the parameter list to just those that Get-ChildItem supports. Thus, any custom ones must be added to this list. The one-line function <strong>RemoveIntroducedParameters<\/strong> removes any parameters on this list from the list of parameters supplied during invocation of Get-EnhancedChildItem.<\/p>\n<h3 id=\"step3\">Step 3: Add your parameter(s) to the cmdlet signature<\/h3>\n<p>As with any PowerShell function, you must declare your new parameters in the signature. This may be as simple as just a parameter <em>name<\/em> but I recommend at a minimum also including its <em>type<\/em> (a .NET framework type name). Where appropriate, you should also include a <em>default <\/em><em>value<\/em>.<\/p>\n<p>Of the five parameters I introduced four of them are Boolean and one is a string array. A Boolean parameter is a special case: though you could use a Boolean type it is cleaner to use a Switch type (as shown below). Jeffery Hicks elaborates on this point in his post <a href=\"http:\/\/jdhitsolutions.com\/blog\/2010\/04\/bool-vs-switch\/\">Bool vs. Switch<\/a>. Essentially, using Switch allows you to just use -MyBoolParam whereas using a Boolean requires writing -MyBoolParam:$True. In either case, though, the parameter is automatically set to false if not specified at invocation (reference: <a href=\"http:\/\/technet.microsoft.com\/en-us\/library\/dd878252(v=VS.85).aspx\">Switch Parameters<\/a> on MSDN) so no explicit default value is needed here for the Switch parameters:<\/p>\n<pre class=\"lang:c# theme:vs2012\">\u00a0\u00a0\u00a0\u00a0[System.String[]]\r\n\u00a0\u00a0\u00a0\u00a0${ExcludeTree} = @(),\r\n\r\n\u00a0\u00a0\u00a0\u00a0[Switch]\r\n\u00a0\u00a0\u00a0\u00a0${Svn},\r\n\r\n\u00a0\u00a0\u00a0\u00a0[Switch]\r\n\u00a0\u00a0\u00a0\u00a0${ContainersOnly},\r\n\r\n\u00a0\u00a0\u00a0\u00a0[Switch]\r\n\u00a0\u00a0\u00a0\u00a0${NoContainersOnly},\r\n\u00a0\u00a0\u00a0\u00a0[Switch]\r\n\u00a0\u00a0\u00a0\u00a0${FullName} <\/pre>\n<p>You can, of course, add further specification to your parameters. If you examine the Get-EnhancedChildItem code, you will see, for example, some parameters specifying position, alias, mandatory, etc. See the standard PowerShell help texts <a href=\"http:\/\/technet.microsoft.com\/en-us\/library\/dd315296.aspx\">about_Parameters<\/a> and <a href=\"http:\/\/technet.microsoft.com\/en-us\/library\/dd347600.aspx\">about_Functions_Advanced_Parameters<\/a> for further details.<\/p>\n<h3 id=\"step4\">Step 4: Document your added functionality<\/h3>\n<p>It is always good practice to document your API, even if only for internal use, but definitely when you produce a library for public use. There are two areas to consider: the parameter-specific descriptions, and the general functionality description. Here is the parameter-specific help included with Get-EnhancedChildItem:<\/p>\n<pre class=\"lang:c# theme:vs2012\">.PARAMETER ExcludeTree\r\nExcludes not just a matching item but also all its children as well.\r\nWildcards are permitted.\r\n\r\n.PARAMETER Svn\r\nIgnores files and folders that are not Subversion-aware.\r\n\r\n.PARAMETER NoContainersOnly\r\nReturns only non-containers (files).\r\nMutually exclusive with -ContainersOnly.\r\n\r\n.PARAMETER ContainersOnly\r\nReturns only containers (directories).\r\nMutually exclusive with -NoContainersOnly.\r\n\r\n.PARAMETER FullName\r\nRetrieves only the full names of the items in the locations. If you pipe\r\nthe output of this command to another command, only the item full names\r\nare sent. Mutually exclusive with -Name. <\/pre>\n<p>With that in place you can query help for a specific parameter, for example:<\/p>\n<pre class=\"lang:c# theme:vs2012\">Get-Help Get-EnhancedChildItem -Parameter ExcludeTree <\/pre>\n<p>&#8230;which returns this text:<\/p>\n<pre>-ExcludeTree \u00a0\u00a0\u00a0\u00a0Excludes not just a matching item but also all its children as well.\r\n\u00a0\u00a0\u00a0\u00a0Wildcards are permitted.\r\n\r\n\u00a0\u00a0\u00a0\u00a0Required?\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0false\r\n\u00a0\u00a0\u00a0\u00a0Position?\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0named\r\n\u00a0\u00a0\u00a0\u00a0Default value \r\n\u00a0\u00a0\u00a0\u00a0Accept pipeline input?\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0false\r\n\u00a0\u00a0\u00a0\u00a0Accept wildcard characters? \r\n[]&gt;<\/pre>\n<p>If your parameter can be completely explained in isolation, the parameter-specific help text may be sufficient. But, in general, you should describe your new functionality in the broader context of the function, in the .DESCRIPTION section.<\/p>\n<h3 id=\"customizinganycmdlet\">Customizing Any Cmdlet<\/h3>\n<p>Previous sections detailed how to tinker with Get-EnhancedChildItem, introducing further customizations beyond Get-ChildItem for your own purposes. This section discusses where the remaining bits of code not yet discussed, the arcane bits(!), originated from, and thus provides the techniques to customize any cmdlet.<\/p>\n<p>The <a href=\"http:\/\/blogs.msdn.com\/b\/powershell\/archive\/2009\/01\/04\/extending-and-or-modifing-commands-with-proxies.aspx\">MetaProgramming<\/a> PowerShell library developed by <a href=\"http:\/\/www.jsnover.com\/blog\/\">Jeffrey Snover<\/a>, the inventor of PowerShell, provides the <strong>New-<\/strong><strong>ProxyCommand<\/strong> cmdlet. The synopsis in the help text for this cmdlet is decidedly understated: <em>Generate a script for a <\/em><em>ProxyCommand<\/em><em> to call a base Cmdlet adding or removing parameters.<\/em> In other words, it provides a base script for extending an existing cmdlet allowing you to easily customize it. Generating the base script is simple:<\/p>\n<pre class=\"lang:c# theme:vs2012\">New-ProxyCommand ExistingCmdletName [options] &gt; YourCmdletFile.ps1 <\/pre>\n<p>The options are, well, optional. The principal ones to know are AddParameter and RemoveParameter, used to tailor the signature during creation of the script. But once you have generated the script into a file, you can edit it manually to add or remove parameters later as your needs change. RemoveParameter is self-contained, simply omitting the named parameter from the signature. AddParameter adds a new parameter but, as you have seen, you still have to implement the new functionality behind it.<\/p>\n<p>Once you have a new file (e.g. YourCmdletFile.ps1) you can use it in its &#8220;raw&#8221; form by simply executing the file, e.g.:<\/p>\n<pre>.\\YourCmdletFile.ps1 [options] <\/pre>\n<p>But assuming you want to reuse this code, it makes sense to wrap the generated code into a function so you have a new, named cmdlet. This is trivial to do. The generated file has the sections indicated below in black: just add the two red lines, substituting the name of your new cmdlet:<\/p>\n<pre class=\"lang:c# theme:vs2012\">Function NewCmdletName {\r\nParameters\r\nBegin-block\r\nProcess-block\r\nEnd-block\r\n} <\/pre>\n<p>Andy Schneider, in his blog post <a href=\"http:\/\/get-powershell.com\/post\/2009\/01\/05\/Using-Proxy-Commands-in-PowerShell.aspx\">Using Proxy Commands in PowerShell<\/a>, shows this for the very same cmdlet under discussion here, Get-ChildItem. I mention it here because you may want to review that code before browsing Get-EnhancedChildItem; the former is easier to digest because it only includes two simple parameters and does not have all the bells and whistles of Get-EnhancedChildItem. (It is, in fact, where I started this whole exploration!)<\/p>\n<p>Be aware, though, that New-ProxyCommand does <em>not<\/em> script dynamic parameters, as I mentioned earlier in the article. If you want your new cmdlet to be a superset of the base cmdlet, you must manually add the dynamic parameters to your new script&#8217;s signature. Depending on how particular you want (or need) to be, you could add these as regular parameters (but thereby exposing possible incorrect functionality if the user invokes these on an invalid provider) or as dynamic parameters (requiring more coding on your part).<\/p>\n<h2 id=\"conclusion\">Conclusion<\/h2>\n<p>When I was first starting to learn PowerShell I was quite surprised that Get-ChildItem did not include an option to prune subtrees. Once I had enough experience under my belt I set off on an adventure (almost anything substantial in PowerShell is an adventure the first time you do it!) to provide this vital enhancement. Along the way I developed a flexible strategy for extending the cmdlet with several new filtering choices, and I was eager to share this with the developer community. Thanks to Simple-Talk for providing a venue for me to do this.<\/p>\n<\/div>\n","protected":false},"excerpt":{"rendered":"<p>One of the most radical features of PowerShell is amongst  the least known. It is possible to extend the buit-in Cmdlets to provide extra functionality. One can add or remove parameters to make subsequent scripting simpler. Michael shows how this is done to meet a practical requirement:, excluding entire subtrees from a recursive directory trawl for automating source control. &hellip;<\/p>\n","protected":false},"author":221868,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[143538],"tags":[4143,4229,5636,5637,4635,4179],"coauthors":[6802],"class_list":["post-1323","post","type-post","status-publish","format-standard","hentry","category-dotnet-development","tag-net","tag-net-framework","tag-file-trees","tag-filtering","tag-powershell","tag-source-control"],"acf":[],"_links":{"self":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/1323","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/users\/221868"}],"replies":[{"embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/comments?post=1323"}],"version-history":[{"count":8,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/1323\/revisions"}],"predecessor-version":[{"id":73022,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/1323\/revisions\/73022"}],"wp:attachment":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/media?parent=1323"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/categories?post=1323"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/tags?post=1323"},{"taxonomy":"author","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/coauthors?post=1323"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}