Powershell - Splitting string into separate components - powershell

I am writing a script which will basically do the following:
Read from a text file some arguments:
DriveLetter ThreeLetterCode ServerName VolumeLetter Integer
Eg. W MSS SERVER01 C 1
These values happen to form a folder destination W:\MSS\, and a filename which works in the following naming convention:
SERVERNAME_VOLUMELETTER_VOL-b00X-iYYY.spi - Where The X is the Integer above
The value Y I need to work out later, as this happens to be the value of the incremental image (backups) and I need to work out the latest incremental.
So at the moment --> Count lines in file, and loop for this many lines.
$lines = Get-Content -Path PostBackupCheck-Textfile.txt | Measure-Object -Line
for ($i=0; $i -le $lines.Lines; $i++)
Within this loop I need to do a Get-Content to read off the line I am currently looking at i.e. line 0, line 1, line 2, as there will be multiple lines in the format I wrote at the beginning and split the line into an array, whereby each part of the file, as seen above naming convention, is in a[0], a[1], a[2]. etc
The reason for this is because, I need to then sort the folder that contains these, find the latest file, by date, and take the _iXXX.spi part and place this into the array value a[X] so I then have a complete filename to mount. This value will replace iYYY.spi
It's a little complex because I also have to make sure when I do a Get-ChildItem with -Include before I sort it all by date, I am only including the filename that matches the arguments fed to it from the text file :
So,
SERVER01_C_VOL-b001-iYYY.spi and not anything else.
i.e. not SERVER01_D_VOL-b001-iYYY.spi
Then take the iYYY value from the sort on the Get-ChildItem -Include and place that into the appropriate array item.
I've literally no idea where to start, so any ideas are appreciated!
Hopefully I've explained in enough detail. I have also placed the code on Pastebin: http://pastebin.com/vtFifTW6

This doesn't need to be that complex. You can start by operating over lines in your file with a simple pipeline:
Get-Content PostBackupCheck-Textfile.txt |
Foreach-Object {
$drive, $folder, $server, $volume, [int]$i = -split $_
...
}
The line inside the loop splits the current input line at spaces and assigns appropriate variables. This saves you the trouble of handling an array there. Everything that follows needs to be in said loop as well.
You can then construct the file name pattern:
$filename = "$server_$drive_VOL-b$($i.ToString('000'))-i*.spi"
which you can use to find all fitting files and sort them by date:
$lastFile = Get-ChildItem $filename | sort LastWriteTime | select -last 1

Related

removing the last line of files using PowerShell command

I would like to remove last line of few files using PowerShell command. I saw the following which does the same thing for the first line of the files. How can I modify that to remove the last line of the files?
gci *.txt | % { (gc $_) | ? { (1) -notcontains $_.ReadCount } | sc -path $_ }
I will appreciate it if I also get the explanation for the commands.
Cheers,
Siavoush
I haven't try anything yet.
Undoubtedly there are a number of solutions for this.
The below one should perform quite fast:
# create a List object to store the contents of the files
$list = [System.Collections.Generic.List[string]]::new()
Get-ChildItem -Path 'X:\Where\The\Files\Are' -Filter '*.txt' -File | ForEach-Object {
$list.AddRange([System.IO.File]::ReadAllLines($_.FullName))
$list.RemoveAt($list.Count - 1)
[System.IO.File]::WriteAllLines($_.FullName, $list)
$list.Clear()
}
Some explanation to the code above:
Because an array is static, adding or removing elements from it is a time and memory consuming thing to do because the entire array would need to be recreated in memory.
This is why I chose to wrk with the System.Collections.Generic.List object that is optimized for adding or removing elements.
Instead of using Get-Content, I chose to use .Net [System.IO.File]::ReadAllLines() method because that has less overhead and performs faster when reading an entire text file as array of lines
$list.RemoveAt($list.Count - 1) takes away the last element (line) from the array of lines
[System.IO.File]::WriteAllLines($_.FullName, $list) rewrites the lines (minus the last because we removed that) back to the file, overwriting the original content
finally, $list.Clear() empties the List object to get it ready for the next file

Renaming several files using CSV in Powershell

I have a special need and I feel stuck on that..
Some user will put some file in a directory with several different name, and I need to rename them regarding a special pattern for those files to be consume by another app.
Example:
In directory -> Target
file1-dd-mm-yyyy -> file1
file2 -> thisfile2
flie45224 -> file123
So as you can see this can be some variables, dates, ID etc..
The target will always be the same but the file in source can be different because of date for example.
So first I had 2 files, so I write the script in plain text "If test-path blabla do this else go out" but it seems that now I will have 37 different files with all differents name. So I thought about using an excel sheet(CSV?), but I can't really find any help on this need.
The goal is to have a sheet as "pattern" like if you found file like in 'A1' then rename as in 'A2' etc...
Do you guys have an idea ?
Thanks by advance :)
I understand you need a csv with the following columns:
A1: regex/pattern to match
A2: transform rule which should be
dynamic
The trick is to use scriptblock if you want to use variables like the file name.
Basically, your csv will be:
A1;A2
"^file1-\d\d-\d\d-\d\d\d\d$";"file1"
"^file2$";"this$($file.Name)"
"^flie*";"file123"
And the code would be:
$myRules = Import-Csv "C:\xxx\test.csv" -Delimiter ";"
$files = gci C:\temp\
foreach ($file in $files) {
foreach ($rule in $myRules) {
if ($file.Name -match $rule.A1) {
Write-host "$($file.Name) is matching pattern ""$($rule.A1)"". Applying rename to ""$($rule.A2)"""
$NewScriptBlock = [scriptblock]::Create("rename-item -Path $($file.FullName) -NewName ""$($rule.A2)""")
$NewScriptBlock.Invoke()
}
}
}
Which gives before:
file1-01-02-0344
file2
flie45224
Output during the execution:
file1-01-02-0344 is matching pattern "^file1-\d\d-\d\d-\d\d\d\d$". Applying rename to "file1"
file2 is matching pattern "^file2$". Applying rename to "this$($file.Name)"
flie45224 is matching pattern "^flie*". Applying rename to "file123"
And after:
file1
thisfile2
file123
Explanations
The first foreach is parsing the files. Then for each of those files, we are checking if one of the rule is matching thanks to the -match $rule.A1.
With the first example, you can use regexp (\d to match digits). For the other cases, I kept it simple as you didn't clarify the rules but this will be your homework :)
Then in the transform rules, you can use the filename variable as shown in the second transform rule: this$($file.Name)
NB: it could be a good idea to add a flag to leave the loop for the current file once it has been renamed to avoid unecessary check and to display a message if the file hasn't match any pattern.
Seems a bit odd, I suspect you should be able to use a regex pattern instead but can't say without seeing the csv contents. Otherwise try this. Assumes the CSV has a column headers called First and Second, First matches the basename (no extension) of the file and Second contains the basename you want it changed to.
$csv = Import-Csv -Path c:\filenames.csv
Get-ChildItem -Path c:\temp | %{if($csv.First -contains $_.BaseName){Rename-Item -Path $_.FullName -NewName "$($csv.Second[$csv.First.IndexOf($_.BaseName)]+$_.Extension)"}}

Delete a file, if it is empty except for a header row

I am trying to write a PowerShell script to delete a file if its empty, apart from the header.
postanote's answer provides some useful background information on the use of the Measure-Object cmdlet.
In the case at hand, however, it's simpler and faster to use the following:
$file = 'C:\path\to\FileOfInterest'
if ((Get-Content -First 2 $file).Count -le 1) {
Remove-Item $file
}
Get-Content -First 2 $file returns up to 2 lines from the start of file $file, as an array.
Note:-First is a more descriptive alias for the -TotalCount parameter; in PowerShell v2, use the latter.
(...).Count counts the elements of that array, i.e., the number of lines actually read.[1]
-le 1 (-le meaning less-than-or-equal) returns $true if, despite asking for 2 lines, only 0 or 1 are returned.
The Remove-Item call then removes file $file.
[1] Up to PowerShell version 2, .Count would return $null if only 1 line had been read, because PowerShell returns a single output object as-is instead of wrapping it in a single-element array. However, since $null is coerced to 0 in a numerical comparison such as with -le, ths solution works in v2 as well. PowerShell versions 3 and higher implicitly implement a .Count property even on scalars (single objects), which - sensibly - returns 1.
Agreed Olaf...
Khader - What did you search for. There are samples of how to count lines in a file all over the web.
Just search for 'powershell count lines in file'
Example hits.
Use a PowerShell Cmdlet to Count Files, Words, and Lines
How to count number of lines and words in a file using Powershell?
If I want to know how many lines are contained in the file, I use the
Measure-Object cmdlet with the line switch. This command is shown
here:
Get-Content C:\fso\a.txt | Measure-Object –Line
If I need to know the number of characters, I use the character
switch:
Get-Content C:\fso\a.txt | Measure-Object -Character
There is also a words switched parameter that will return the number
of words in the text file. It is used similarly to the character or
line switched parameter. The command is shown here:
Get-Content C:\fso\a.txt | Measure-Object –Word
In the following figure, I use the Measure-Object cmdlet to count
lines; then lines and characters; and finally lines, characters, and
words. These commands illustrate combining the switches to return
specific information.
Update for OP.
You should have updated your original question for context vs putting your code in the comment
As for …
Is there any way I can return just the count and use it with an if
statement to check if it is equal to 1, and then del the file
Just use the if statement when checking for the 'lines' count greater than 1
If (Get-Content $_.FullName | Measure-Object –Line | Where-Object -Property Lines -gt 1)
{
'Count is greater than one'
Remove-Item ...
}
Again, this is very basic PowerShell overview stuff, so it's prudent you take Olaf's suggestion to limit future confusion, frustrations, misconceptions and errors you are going to encounter.

Conditional Rename of Multiple Files - Powershell

Scenario: Folder with more than one file(There are a maximum of 5 files). Each file starts with a character(does not repeat) followed by numbers. e.g: A123,B234,C123...
Objective: Rename the files according to a predetermined mapping. e.g: if A=1, B=2 etc. Then the File Starting with "A" becomes "1.", the file starting with "B" becomes "2." and so on. e.g: A123 => 1.A123
My Solution: I am not fluent in PowerShell but here is my attempt in achieving the above objective.
powershell "cd C:\Temp ; dir | ForEach-Object{if ($_.Name -Like "A*") {Rename-Item $_ "1.$_"} else {if ($_.Name -like "B*") {Rename-Item $_ "2.$_"} else{if($_.Name -like "C*"){Rename-Item $_ "3.$_"}}}}"
I needed the script to be executed from cmd and also in a specific folder (hence the cd and then the composed rename command).
This gets the job done but I would really appreciate if anyone could simplify things and show me a more prettier way at dealing with the situation.
So you can convert a letter to a number using something like:
[int][char]"F"
That will output 70. So, for your need you just need to get the first character of the file name, which is a simple SubString(0,1) call, then run it through ToUpper() to make sure you don't get any lower case letters, and then do the [int][char] bit to it, and subtract 64.
powershell "cd C:\Temp ; dir | ForEach-Object{$NewNameNum = [int][char]$_.Name.Substring(0,1).ToUpper() - 64;Rename-Item $_ "$NewNameNum.$_"}
Edit: Ok, so your original question is misleading, and should be edited to more accurately represent your request. If you are not assigning A=1, B=2, C=3 as a direct translation I can see 2 good options. First is a hashtable lookup.
PowerShell "$NmbrConv = #{'A'=3;'B'=1;'C'=9;'D'=2};dir c:\temp\*|%{$NewNameNum = $NmbrConv[$_.Name.Substring(0,1)];Rename-Item $_ "$NewNameNum.$_"}
This defines what letters convert to what numbers, then for each file just references the hashtable to get the number.
The other option is the Switch command. Running it in-line gets kind of ugly, but here's what it would look like formatted nicely.
Switch(GCI C:\Temp){
"^a" {$NewNameNum=3}
"^b" {$NewNameNum=1}
"^c" {$NewNameNum=9}
"^d" {$NewNameNum=2}
default {Rename-Item $_ "$NewNameNum.$_"}
}
Then if you need it all in one line you remove new lines and replace them with semicolons.
powershell 'Switch(GCI C:\Temp){"^a" {$NewNameNum=3};"^b" {$NewNameNum=1};"^c" {$NewNameNum=9};"^d" {$NewNameNum=2};default {Rename-Item $_ "$NewNameNum.$_"}}'

Rename Files with Index(Excel)

Anyone have any ideas on how to rename files by finding an association with an index file?
I have a file/folder structure like the following:
Folder name = "Doe, John EO11-123"
Several files under this folder
The index file(MS Excel) has several columns. It contains the names in 2 columns(First and Last). It also has a column containing the number EO11-123.
What I would like to do is write maybe a script to look at the folder names in a directory, compare/find an associated value in the index file(like that number EO11-123) and then rename all the files under the folder using a 4th column value in the index.
So,
Folder name = "Doe, John EO11-123", index column1 contains same value "EO11-123", use column2 value "111111_000000" and rename all the files under that directory folder to "111111_000000_0", "111111_000000_1", "111111_000000_2" and so on.
This possible with powershell or vbscript?
Ok, I'll answer your questions in your comment first. Importing the data into PowerShell allows you to make an array in powershell that you can match against, or better yet make a HashTable to reference for your renaming purposes. I'll get into that later, but it's way better than trying to have PowerShell talk to Excel and use Excel's search functions because this way it's all in PowerShell and there's no third party application dependencies. As for importing, that script is a function that you can load into your current session, so you run that function and it will automatically take care of the import for you (it opens Excel, then opens the XLS(x) file, saves it as a temp CSV file, closes Excel, imports that CSV file into PowerShell, and then deletes the temp file).
Now, you did not state what your XLS file looks like, so I'm going to assume it's got a header row, and looks something like this:
FirstName | Last Name | Identifier | FileCode
Joe | Shmoe | XA22-573 | JS573
John | Doe | EO11-123 | JD123
If that's not your format, you'll need to either adapt my code, or your file, or both.
So, how do we do this? First, download, save, and if needed unblock the script to Import-XLS. Then we will dot source that file to load the function into the current PowerShell session. Once we have the function we will run it and assign the results to a variable. Then we can make an empty hashtable, and for each record in the imported array create an entry in the hashtable where the 'Identifier' property (in your example above that would be the one that has the value "EO11-123" in it), make that the Key, then make the entire record the value. So, so far we have this:
#Load function into current session
. C:\Path\To\Import-XLS.ps1
$RefArray = Import-XLS C:\Path\To\file.xls
$RefHash = #{}
$RefArray | ForEach( $RefHash.Add($_.Identifier, $_)}
Now you should be able to reference the identifier to access any of the properties for the associated record such as:
PS C:\> $RefHash['EO11-123'].FileCode
JD123
Now, we just need to extract that name from the folder, and rename all the files in it. Pretty straight forward from here.
Get-ChildItem c:\Path\to\Folders -directory | Where{$_.Name -match "(?<= )(\S+)$"}|
ForEach{
$Files = Get-ChildItem $_.FullName
$NewName = $RefHash['$($Matches[1])'].FileCode
For($i = 1;$i -lt $files.count;$i++){
$Files[$i] | Rename-Item -New "$NewName_$i"
}
}
Edit: Ok, let's break down the rename process here. It is a lot of piping here, so I'll try and take it step by step. First off we have Get-ChildItem that gets a list of folders for the path you specify. That part's straight forward enough. Then it pipes to a Where statement, that filters the results checking each one's name to see if it matches the Regular Expression "(?<= )(\S+)$". If you are unfamiliar with how regular expressions work you can see a fairly good breakdown of it at https://regex101.com/r/zW8sW1/1. What that does is matches any folders that have more than one "word" in the name, and captures the last "word". It saves that in the automatic variable $Matches, and since it captured text, that gets assigned to $Matches[1]. Now the code breaks down here because your CSV isn't laid out like I had assumed, and you want the files named differently. We'll have to make some adjustments on the fly.
So, those folder that pass the filter will get piped into a ForEach loop (which I had a typo in previously and had a ( instead of {, that's fixed now). So for each of those folders it starts off by getting a list of files within that folder and assigning them to the variable $Files. It also sets up the $NewName variable, but since you don't have a column in your CSV named 'FileCode' that line won't work for you. It uses the $Matches automatic variable that I mentioned earlier to reference the hashtable that we setup with all of the Identifier codes, and then looks at a property of that specific record to setup the new name to assign to files. Since what you want and what I assumed are different, and your CSV has different properties we'll re-work both the previous Where statement, and this line a little bit. Here's how that bit of the script will now read:
Get-ChildItem c:\Path\to\Folders -directory | Where{$_.Name -match "^(.+?), .*? (\S+)$"}|
ForEach{
$Files = Get-ChildItem $_.FullName
$NewName = $Matches[2] + "_" + $Matches[1]
That now matches the folder name in the Where statement and captures 2 things. The first thing it grabs is everything at the beginning of the name before the comma. Then it skips everything until it gets tho the last piece of text at the end of the name and captures everything after the last space. New breakdown on RegEx101: https://regex101.com/r/zW8sW1/2
So you want the ID_LName, which can be gotten from the folder name, there's really no need to even use your CSV file at this point I don't think. We build the new name of the files based off the automatic $Matches variable using the second capture group and the first capture group and putting an underscore between them. Then we just iterate through the files with a For loop basing it off how many files were found. So we start with the first file in the array $Files (record 0), add that to the $NewName with an underscore, and use that to rename the file.