Forum Discussion

Thisisit's avatar
Thisisit
Copper Contributor
May 21, 2022
Solved

Select-Object Substring Blank

Hello, thank you for you assistance in advance.  I'm trying to grab the last 10 characters of the Directory.  For some reason it's returning blank, and I'm not able to figure out why.  Below is what I have.  Appreciate any assistance.

 

 

Get-ChildItem -Filter *.txt -Recurse "C:\SamIam\Listings" | Where { ! $_.PSIsContainer } | Select-Object @{Name='Directory2';e={Substring(Directory.length - 10,10)}},Name,Directory

 

  • Thisisit 

     

    Hi.

     

    You're likely getting a blank because there's not such function named "Substring" in PowerShell. What does exist is the Substring() method on a string object, but you're not referencing that, hence the blank result.

     

    Secondly, the property "Directory" is of type [System.IO.FileSystemInfo], not [System.String]. PowerShell can and does do various kind of implicit conversions but it's inefficient, occasionally ambiguous and therefore hard to debug for the uninitiated. Instead of "Directory", use the "DirectoryName" property, as it is of type [System.String].

     

    You do not need the "Where" clause in this example. Get-ChildItem provides a parameter named "-File" which does what your "Where" clause does, just more efficiently.

     

    Lastly, you need to perform some bounds checking as a call .Substring() cannot have a negative number for the starting position, else it will throw a .NET exception.

     

    With those considerations in mind, here's an adjusted version of your command that does what you've asked.

     

    Get-ChildItem -File -Filter *.txt -Recurse -Path "C:\SamIam\Listings" | Select-Object @{Name='Directory2';e={ if ($_.DirectoryName.Length -ge 10) { $_.DirectoryName.Substring($_.DirectoryName.Length - 10) } else { $_.DirectoryName } }},Name,DirectoryName

     

     

    Cheers,

    Lain

6 Replies

  • fatherjack's avatar
    fatherjack
    Copper Contributor

    Thisisit 

    Other answers have mentioned a few issues with your script that I would agree with but I also wanted to present an alternate method of retrieving the information that you wanted.

    Line 2 uses a Regular Expression to match the last 10 non-space characters of the DirectoryName property and then line 6 references the $Matches variable for that value for the output.

    get-childitem c:\windows\system32 -file -recurse -ea silentlycontinue |foreach-object { 
        $null = $_.DirectoryName -match '(?<LastTen>\S{10})$'
                [PSCustomObject]@{
                Name = $_.name
                Directory = $_.DirectoryName
                Directory_Last10 = $matches.LastTen
            }
        }

     

    • LainRobertson's avatar
      LainRobertson
      Silver Contributor

      fatherjack 

       

      Yeah, you definitely can do that. The only thing you'd need to change is the "{10}" would need to be "{1,10}". If you only specify {10}, then you'll fail to produce a match for strings with a length less than 10, which in turn results in a $null value.

       

       

      Other considerations

      Because we're talking about dealing with the file system, there's a pretty good chance performance wouldn't be an issue even if it's over ten or hundreds of thousands of files, but if performance were a requirement and the volume was substantial, a Substring() call would noticeably outpace the Regex methods.

       

      This is going to look quite ugly in comparison to your more readable version - primarily because of the lengthy way in which you have to handle the options, but if I were to Regex it, I'd go via .NET directly.

       

      Get-ChildItem -File -Filter *.txt -Recurse -Path "D:\Data" |
          Select-Object @{Name='Directory2';e={ [regex]::Match($_.DirectoryName, ".{1,10}$", [System.Text.RegularExpressions.RegexOptions]::RightToLeft -bor [System.Text.RegularExpressions.RegexOptions]::IgnoreCase -bor [System.Text.RegularExpressions.RegexOptions]::CultureInvariant).Value }},Name,DirectoryName

       

      Cheers,

      Lain

      • LainRobertson's avatar
        LainRobertson
        Silver Contributor

        It's a slow enough night that I figured I'd use this scenario to take a quick peek at the overhead on PowerShell's "-match".

         

        The testing is hardly rigorous given the low number of iterations for each statement, but it's still interesting.

         

        I'll drop the test statements below but here's the takeaway from 20 iterations of each test type:

         

        Test typeDurationEffective run time
        Baseline~ 2 secondsn/a
        .Substring()~ 2.97 seconds970 ms
        .NET Regex.Match().Value~ 3.55 seconds1.55 seconds
        PowerShell -match~ 4.87 seconds2.87 seconds

         

        Test statements:

        # Baseline measurement
        $sw = [System.Diagnostics.Stopwatch]::StartNew(); for ($i = 0; $i -lt 1000000; $i++) { $n = "Prefix$($r.Next(1000, 100000))"; }; $sw.Stop(); $sw;
        
        # .Substring() test
        $sw = [System.Diagnostics.Stopwatch]::StartNew(); for ($i = 0; $i -lt 1000000; $i++) { $n = "Prefix$($r.Next(1000, 100000))"; if ($g.Length -lt 10) { $o = $g } else { $o = $g.Substring($g.Length - 10) } }; $sw.Stop(); $sw;
        
        # .NET Regex.Match().Value test
        $sw = [System.Diagnostics.Stopwatch]::StartNew(); for ($i = 0; $i -lt 1000000; $i++) { $n = "Prefix$($r.Next(1000, 100000))"; $o = [regex]::Match($_.DirectoryName, ".{1,10}$", [System.Text.RegularExpressions.RegexOptions]::RightToLeft -bor [System.Text.RegularExpressions.RegexOptions]::IgnoreCase -bor [System.Text.RegularExpressions.RegexOptions]::CultureInvariant).Value }; $sw.Stop(); $sw;
        
        # PowerShell -match test
        $sw = [System.Diagnostics.Stopwatch]::StartNew(); for ($i = 0; $i -lt 1000000; $i++) { $n = "Prefix$($r.Next(1000, 100000))"; $null = $n -match ".{1,10}$"; $o = $Matches[0] }; $sw.Stop(); $sw;

         

        PowerShell is really meant for administration meaning these metrics really don't count for much, but it's still interesting (though maybe only to me.)

         

        Cheers,

        Lain

  • LainRobertson's avatar
    LainRobertson
    Silver Contributor

    Thisisit 

     

    Hi.

     

    You're likely getting a blank because there's not such function named "Substring" in PowerShell. What does exist is the Substring() method on a string object, but you're not referencing that, hence the blank result.

     

    Secondly, the property "Directory" is of type [System.IO.FileSystemInfo], not [System.String]. PowerShell can and does do various kind of implicit conversions but it's inefficient, occasionally ambiguous and therefore hard to debug for the uninitiated. Instead of "Directory", use the "DirectoryName" property, as it is of type [System.String].

     

    You do not need the "Where" clause in this example. Get-ChildItem provides a parameter named "-File" which does what your "Where" clause does, just more efficiently.

     

    Lastly, you need to perform some bounds checking as a call .Substring() cannot have a negative number for the starting position, else it will throw a .NET exception.

     

    With those considerations in mind, here's an adjusted version of your command that does what you've asked.

     

    Get-ChildItem -File -Filter *.txt -Recurse -Path "C:\SamIam\Listings" | Select-Object @{Name='Directory2';e={ if ($_.DirectoryName.Length -ge 10) { $_.DirectoryName.Substring($_.DirectoryName.Length - 10) } else { $_.DirectoryName } }},Name,DirectoryName

     

     

    Cheers,

    Lain

    • Thisisit's avatar
      Thisisit
      Copper Contributor
      Thank you all for your assistance, and the various options! Great help, and much appreciated!
  • Thisisit I couldn't get it to work nicely in one line so.. a bit larger script:

     

     

     

    #Set total variable to null
    $total = @()
    
    #Set directory to scan
    $directory = 'D:\test'
    
    #Get all txtfiles from $directory path and put them in $variable
    #If length of directory name is greater than 10, select last ten characters
    #If length is shorter, just add it to the list
    foreach ($txtfile in Get-ChildItem -filter *.txt -Recurse -Path $directory | Where-Object { -not $_.PSISContainer }) {
        if ($txtfile.Directory.name.length -gt 10) {
            $data = [PSCustomObject]@{
                Directory = $txtfile.DirectoryName.substring($txtfile.DirectoryName.length - 10, 10)
                Name      = $txtfile.Name
            }
        }
        else {
            $data = [PSCustomObject]@{
                Directory = $txtfile.Directory.Name
                Name      = $txtfile.Name
            }  
        }
        $total += $data
    }
    
    #output found items
    $total

     

     

     

    This will result in a output like this:

     

     

     

    Directory  Name 
    ---------  ----
    test       x.txt
    1234567890 y.txt
    qrstuvwxyz z.txt

     

     

     

    Test directories were like this:

     

Resources