File directory as variables in Powershell - powershell

I have to get the File directory the script is in as an variable and then split into following elements:
Example Path C:\Name\Season 1\script.ps1
Now what I would need is a variable that would be
var1 = "Name" and
var2 = "1" which needs to just take the Number from "Season 1"
Structure will always be the same --> C:\var1\Season var2\script.ps1 just different names
Thanks in adanvce for everyone willing to help me out as I have no clue on Powershell xd

You can get the full path to the currently executing script using the $PsScriptRoot automatic variable. You can then use the -match operator with Regular Expressions to get the parts you want:
$PsScriptRoot -match '\\(?<Var1>.*)\\Season (?<Var2>\d+)' | Out-Null
Assuming there is a match, then the values you want will be placed in the $Matches automatic variable. So, if your script is C:\MyFavShow\Season 4\Script.ps1, then the you can get the values after running the above by simply outputting Matches:
$Matches
Which would show:
Name Value
---- -----
Var2 4
Var1 MyFavShow
0 \Directory1\Season 4
Access the items individually like this: $Matches.Var1, which would output MyFavShow.
A couple of notes:
$PsScriptRoot isn't populated from the command-line, only when running a script file.
The same matching technique can be used against any path, not just the script root. So, if you enumerate some directories (using, say, Get-ChildItem), you can still match against them.
$Matches does not reset after every use, only if a match is made, so if you do enumerate a lot of paths and check each one, you probably want to also check for an actual match before using the variables (as they may still be set to whatever the previous match was):
if($directory -match '\\(?<Var1>.*)\\Season (?<Var2>\d+)') {
# Do something with the values
}
You can reset $Matches using: $Matches.Clear()

Related

Get original script name included script name and possibly others

I've been testing $MyInvocation, $MyCommand and inspecting some other variables but would like a good example of how to do the following:
Get the name of the current script file
Get the name of the caller script file that included the current script file
Get the name of the original script file that ultimately resulted in #1 being executed
I have used this for original but maybe it will also be caller?
$myInvocation.MyCommand.Name #original
Even if there are more files involved, I only need the current, caller and original. Consider three scripts; if you were to execute first.ps1:
first.ps1
. "$PSScriptRoot\second.ps1"
second.ps1
. "$PSScriptRoot\third.ps1"
#get $Current = second.ps1
#get $Caller = first.ps1
#get $Original = first.ps1
third.ps1
#get $Current = third.ps1
#get $Caller = second.ps1
#get $Original = first.ps1
I have worked out how to get the filename from the path etc...
Also, why is syntax highlighting wonky on SO for PowerShell?
Place the following in second.ps1 and third.ps1, which utilizes the automatic $PSCommandPath variable and Get-PSCallStack:
# Get the full paths of the scripts on the call stack (the calling scripts).
$callingScripts = (Get-PSCallStack).InvocationInfo.PSCommandPath -ne $null
# For illustration, use a custom object to show the current script's
# file name, that of the immediate caller, and that of the original caller.
[pscustomobject] #{
Current = Split-Path -Leaf $PSCommandPath
Caller = Split-Path -Leaf $callingScripts[0]
Original = Split-Path -Leaf $callingScripts[-1]
}
Your third.ps1 script would then output the following:
Current Caller Original
------- ------ --------
third.ps1 second.ps1 first.ps1
Explanation:
The automatic $PSCommandPath variable contains the running script's own full file-system path.
Get-PSCallStack outputs the call stack in the form of System.Management.Automation.CallStackFrame instances, bottom-up: that is, the instance at index 0 represents the immediate caller, whereas index -1 - the last array element - represents the top of the call stack and therefore the original caller.
.InvocationInfo.PSCommandPath is an instance of member-access enumeration, which extracts the caller script path's across all stack frames (across all array elements).
Non-script-file callers may be involved as well, notably the initial script block used behind the scenes to submit a command from the interactive prompt, and the .PSCommandPath property of such contains $null.
-ne $null is used to filter out these $null values, so that the array returned contains only the paths of script-file callers.
Split-Path -Leaf is used to extract just the file-name portion from the full paths.
why is syntax highlighting wonky on SO for PowerShell?
Good question. It seems that some heuristic is being implied to guess the language, and it frequently guesses wrong; As of this writing, explicitly specifying ```powershell at the start of a fenced code block doesn't help, suggesting that PowerShell isn't a supported language. For now, ```sh, even though it's the wrong language, works reasonably well in practice.

Using Powershell environmental variables as strings when outputting files

I am using Get-WindowsAutopilotInfo to generate a computer's serial number and a hash code and export that info as a CSV.
Here is the code I usually use:
new-item "c:\Autopilot_Export" -type directory -force
Set-Location "C:\Autopilot_Export"
Get-WindowsAutopilotInfo.ps1 -OutputFile Autopilot_CSV.csv
Robocopy C:\Autopilot_Export \\Zapp\pc\Hash_Exports /copyall
This outputs a CSV file named "Autopilot_CSV.csv" to the C:\Autopilot_Export directory and then Robocopy copies it to the Network Share drive inside of the Hash_Exports folder listed above. In the above code, if I manually type in "test", "123", "ABC", etc. and replace "Autopilot_CSV" it will output a CSV under all of those names as well. So it looks like Get-WindowsAutopilotInfo will create a CSV file and save the file name with whatever string you pass into it. Great.
However, I am running this script on multiple different computers and I would like the exported CSV to be named something unique to the machine it is running on so I can easily identify it once it's copied. I am trying to pass the value of the environmental variable $env:computername as a string input for the CSV and it isn't working.
Here's the code I'm trying to use:
new-item "c:\Autopilot_Export" -type directory -force
Set-Location "C:\Autopilot_Export"
Get-WindowsAutopilotInfo.ps1 -OutputFile $env:computername.csv
Robocopy C:\Autopilot_Export C:\Users\danieln\Desktop /copyall
This code does not generate the CSV file at all. Why not?
Do I just have a basic misunderstanding of how environmental variables are used in Powershell? $env:computername appears to return a string of the computer's name, but I cannot pass it into Get-WindowsAutopilotInfo and have it save, but it will work if I manually type a string in as the input.
I have also tried setting it to a variable $computername = [string]$env:computername and just passing $computername in before the .CSV and that doesn't appear to work either. And per the docmentation, environmental variables are apparently already strings.
Am I missing something?
Thanks!
js2010's answer shows the effective solution: use "..."-quoting, i.e. an expandable string explicitly.
It is a good habit to form to use "..." explicitly around command arguments that are strings containing variable references (e.g. "$HOME/projects") or subexpressions (e.g., "./folder/$(Get-Date -Format yyyy-MM)")
While such compound string arguments generally do not require double-quoting[1] - because they are implicitly treated as if they were "..."-enclosed - in certain cases they do, and when they do is not obvious and therefore hard to remember:
This answer details the surprisingly complex rules, but here are two notable pitfalls if you do not use "...":
If a compound argument starts with a subexpression ($(...)), its output is passed as a separate argument; e.g. Write-Output $(Get-Location)/folder passes two arguments to Write-Output: the result of $(Get-Location) and literal /folder
If a compound argument starts with a variable reference and is followed by what syntactically looks like either (a) a property access (e.g., $PSVersionTable.PsVersion) or (b) a method call (e.g., $PSHome.Tolower()) it is executed as just that, i.e. as an expression (rather than being considered a variable reference followed by a literal part).
Aside #1: Such an argument then isn't necessarily a string, but whatever data type the property value or method-call return value happens to be.
Aside #2: A compound string that starts with a quoted string (whether single- or double-quoted) does not work, because the quoted string at the start is also considered an expression, like property access and method calls, so that what comes after is again passed as separate argument(s). Therefore, you can only have a compound strings consisting of a mix of quoted and unquoted substrings if the compound string starts with an unquoted substring or a non-expression variable reference.[2]
The latter is what happened in this case:
Unquoted $env:computername.csv was interpreted as an attempt to access a property named csv on the object stored in (environment) variable $env:computername, and since the [string] instance stored there has no csv property, the expression evaluated to $null.
By forcing PowerShell to interpret this value as an expandable string via "...", the usual interpolation rules apply, which means that the .csv is indeed treated as a literal (because property access requires use of $(...) in expandable strings).
[1] Quoting is required for strings that contain metacharacters such as spaces.
For values to be treated verbatim, '...' quoting is the better choice (see the bottom section of this answer for an overview of PowerShell string literals).
Also, using neither double- nor single-quoting and individually `-escaping metacharacters is another option (e.g. Write-Output C:\Program` Files.
[2] For instance, Write-Output a"b`t"'`c' and Write-Output $HOME"b`t"'`c' work as intended, whereas Write-Output "b`t"'`c' and Write-Output $HOME.Length"b`t"'`c' do not (the latter two each pass 3 arguments). The workaround is to either use a single "..." string with internal `-escaping as necessary (e.g. Write-Output "${HOME}b`t``c") or to use string-concatenation expression with + enclosed in (...) (e.g. Write-Output ($HOME + "b`t" + '`c'))
Doublequoting seems to work. The colon is special in powershell parameters.
echo hi | set-content "$env:computername.csv"
dir
Directory: C:\users\me\foo
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 2/11/2021 1:02 PM 4 COMP001.csv
The colon is special. For example in switch parameters:
get-childitem -force:$true
Actually, it's trying to find a property named csv:
echo hi | Set-Content $env:COMPUTERNAME.length
dir
Directory: C:\Users\me\foo
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 2/11/2021 3:04 PM 4 8
Basically pass it a string rather than the variable:
write-host $env:computername.csv
# output: (no output - looking for a variable called "computername.csv" instead of "computername")
Try the following syntax:
$filename = "$($env:computername).csv"
write-host $filename
# output: MYPCNAME.csv

Updating a txt file of variables in powershell

So my challenge today.
I have a config file (really just a txt document) that stores variables to store information passed between scripts or to be used after restarts.
I am looking for a more efficient way to read and update the file. Currently I read the file with:
Get-Content $current\Install.cfg | ForEach-Object {
Set-Variable -Name line -Value $_
$a, $b = $line.Split('=')
Set-Variable -name $a -Value $b
}
But to overwrite the contents, I recreate the file with:
ECHO OSV=$OSV >>"$ConfigLoc\tool.cfg"
ECHO OSb=$OSb >>"$ConfigLoc\tool.cfg"
ECHO cNum=$cNum >>"$ConfigLoc\tool.cfg"
ECHO cCode=$cCode >>"$ConfigLoc\tool.cfg"
ECHO Comp=$Comp >>"$ConfigLoc\tool.cfg"
Each time I have added a new saved variable, I have just hardcoded the new variable into both the original config file and the config updater.
As my next updates require an additional 30 variables to my current 15. I would like something like:
Get-Content $current\Install.cfg | ForEach-Object {
Set-Variable -Name line -Value $_
$a, $b = $line.Split('=')
ECHO $a=$$a
}
Where $$a uses the variable $a in the loop as the variable name to load the value.
Best example i can show to clarify is:
ECHO $a=$$a (in current loop)
Echo OSV=$OSV (actually appears in code as)
Not sure how to clarify this anymore, or how to achieve it with the variable title also being a variable.
If you want to create a file that has name=value parameters, here's an alternate suggestion. This is a snippet of a real script I use every day. You might modify it so it reads your .csv input and uses it instead of the hard coded values.
$Sites = ("RawSiteName|RoleName|DevUrl|SiteID|HttpPort|HttpsPort", `
"SiteName|Name of role|foo.com|1|80|443" `
) | ConvertFrom-CSV -Delimiter "|"
$site = $sites[0]
Write-Host "RawSiteName =$($site.RawSiteName)"
You might be able to use something similar to $text = Get-Content MyParameters.csv and pipe that to the ConvertFrom-CSV cmdlet. I realize it's not a direct answer to what you are doing but it will let you programmatically create a file to pass across scripts.
Thanks for the help everyone. This is the solution I am going with. Importing and exporting couldn't be simpler. If I have to manually update the XML install default I can with ease which is also amazing. I also love the fact that even if you import as $Test you can still use $original to access variables. I will be creating multiple hashtables to organize the different data I will be using going forward and just import/export it in a $config variable as the master.
$original = #{
OSV='0'
OSb='0'
cNum='00000'
cCode='0000'
Client='Unknown'
Comp='Unknown'
}
$original | Export-Clixml $Home\Desktop\sample.cfg
$Test = Import-Clixml $Home\Desktop\sample.cfg
Write $Test
Write $original.Client
In essence, you're looking for variable indirection: accessing a variable indirectly, via its name stored in another variable.
In PowerShell, Get-Variable allows you to do that, as demonstrated in the following:
# Sample variables.
$foo='fooVal'
$bar='barVal'
# List of variables to append to the config file -
# the *names* of the variables above.
$varsToAdd =
'foo',
'bar'
# Loop over the variable names and use string expansion to create <name>=<value> lines.
# Note how Get-Variable is used to retrieve each variable's value via its *name*.
$(foreach ($varName in $varsToAdd) {
"$varName=$(Get-Variable $varName -ValueOnly)"
}) >> "$ConfigLoc/tool.cfg"
With the above, the following lines are appended to the output *.cfg file:
foo=fooVal
bar=barVal
Note that you can read such a file more easily with the ConvertFrom-StringData, which outputs a hashtable with the name-value pairs from the file:
$htSettings = Get-Content -Raw "$ConfigLoc/tool.cfg" | ConvertFrom-StringData
Accessing $htSettings.foo would then return fooVal, for instance.
With a hashtable as the settings container, updating the config file becomes easier, as you can simply recreate the file with all settings and their current values:
$htSettings.GetEnumerator() |
ForEach-Object { "$($_.Key)=$($_.Value)" } > "$ConfigLoc/tool.cfg"
Note: PowerShell by default doesn't enumerate the entries of a hashtable in the pipeline, which is why .GetEnumerator() is needed.
Generally, though, this kind of manual serialization is fraught, as others have pointed out, and there are more robust - though typically less friendly - alternatives.
With your string- and line-based serialization approach, there are two things to watch out for:
All values are saved as a strings, so you have to manually reconvert to the desired data type, if necessary - and even possible, given that not all objects provide meaningful string representations.
Generally, the most robust serialization format is Export-CliXml, but note that it is not a friendly format - be careful with manual edits.
ConvertFrom-StringData will fail with duplicate names in the config file, which means you have to manually ensure that you create no duplicate entries when you append to the file - if you use the above approach of recreating the file from a hashtable every time, however, you're safe.

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.