Multiple variables in path - powershell

I am planning on using the following script by looping through a text file to set variables for the location of the source PDF, and designate the path to create a new folder (with week number) to move the source PDF to.
$pdfSource = 'C:\path\in\text\file'
$newFolder = 'C:\path\to\newfolder\in\text\file'
Get-ChildItem $pdfSource '*.pdf' -Recurse | foreach {
$x = $_.LastWriteTime.ToShortDateString()
$new_folder_name = Get-Date $x -UFormat %V
$des_path = "C:\path\to\newfolder\$new_folder_name"
if (Test-Path $des_path) {
Move-Item $_.FullName $des_path
} else {
New-Item -ItemType Directory -Path $des_path
Move-Item $_.FullName $des_path
}
}
I can't seem to figure out the syntax for the line below to include the $newFolder path variable along with the existing $new_folder_name I'm creating.
$des_path = "C:\path\to\newfolder\$new_folder_name"

Option-1:
$des_path = "${newFolder}\${new_folder_name}"
Option-2:
$des_path = "${0}\${1}" -f $newFolder, $new_folder_name
Option-3:
$des_path = $newFolder + $new_folder_name
Option-4:
$des_path = Join-Path -Path $newFolder -ChildPath $new_folder_name

There's nothing wrong with your approach to string expansion (interpolation):
$new_folder_name = 'foo' # sample value
$des_path = "C:\path\to\newfolder\$new_folder_name" # use string expansion
yields string literal C:\path\to\newfolder\foo, as expected.
Adam's answer shows you alternatives to constructing file paths, with Join-Path being the most robust and PowerShell-idiomatic, albeit slow.
Another option is to use [IO.Path]::Combine():
[IO.Path]::Combine('C:\path\to\newfolder', $new_folder_name)
The way you calculate the value for $new_folder_name should be problematic if your current culture is not en-US (US-English), but due to a bug actually isn't[1]; either way, it should be simplified:
Instead of:
$x = $_.LastWriteTime.ToShortDateString()
$new_folder_name = Get-Date $x -uformat %V
use:
$new_folder_name = Get-Date $_.LastWriteTime -uformat %V
That is, pass $_.LastWriteTime directly to Get-Date, as a [datetime] instance - no detour via a string representation needed.
[1] .ToShortDateString() returns a culture-sensitive string representation, whereas PowerShell typically uses the invariant culture to ensure cross-culture consistency; therefore, if you pass a string to a parameter that accepts a [datetime] instance, it is the invariant culture's formats that should (only) be recognized, not the current culture's. While that is true for functions written in PowerShell, in compiled cmdlets (typically C#-based), the current culture is unexpectedly applied; while this is a bug, a decision was made not to fix it for the sake of backward compatibility - see this GitHub issue

Related

Powershell dropping characters while creating folder names

I am having a strange problem in Powershell (Version 2021.8.0) while creating folders and naming them. I start with a number of individual ebook files in a folder that I set using Set-Location. I use the file name minus the extension to create a new folder with the same name as the e-book file. The code works fine the majority of the time with various file extensions I have stored in an array beginning of the code.
What's happening is that the code creates the proper folder name the majority of the time and moves the source file into the folder after it's created.
The problem is, if the last letter of the source file name, on files with the extension ".epub" end in an "e", then the "e" is missing from the end of the created folder name. I thought that I saw it also drop "r" and "p" but I have been unable to replicate that error recently.
Below is my code. It is set up to run against file extensions for e-books and audiobooks. Please ignore the error messages that are being generated when files of a specific type don't exist in the working folder. I am just using the array for testing and it will be filled automatically later by reading the folder contents.
This Code Creates a Folder for Each File and moves the file into that Folder:
Clear-Host
$SourceFileFolder = 'N:\- Books\- - BMS\- Books Needing Folders'
Set-Location $SourceFileFolder
$MyArray = ( "*.azw3", "*.cbz", "*.doc", "*.docx", "*.djvu", "*.epub", "*.mobi", "*.mp3", "*.pdf", "*.txt" )
Foreach ($FileExtension in $MyArray) {
Get-ChildItem -Include $FileExtension -Name -Recurse | Sort-Object | ForEach-Object { $SourceFileName = $_
$NewDirectoryName = $SourceFileName.TrimEnd($FileExtension)
New-Item -Name $NewDirectoryName -ItemType "directory"
$OriginalFileName = Join-Path -Path $SourceFileFolder -ChildPath $SourceFileName
$DestinationFilename = Join-Path -Path $NewDirectoryName -ChildPath $SourceFileName
$DestinationFilename = Join-Path -Path $SourceFileFolder -ChildPath $DestinationFilename
Move-Item $OriginalFileName -Destination $DestinationFilename
}
}
Thanks for any help you can give. Driving me nuts and I am pretty sure it's something that I am doing wrong, like always.
String.TrimEnd()
Removes all the trailing occurrences of a set of characters specified in an array from the current string.
TrimEnd method will remove all characters that matches in the character array you provided. It does not look for whether or not .epub is at the end of the string, but rather it trims out any of the characters in the argument supplied from the end of the string. In your case, all dots,e,p,u,b will be removed from the end until no more of these characters are within the string. Now, you will eventually (and you do) remove more than what you intended for.
I'd suggest using EndsWith to match your extensions and performing a substring selection instead, as below. If you deal only with single extension (eg: not with .tar.gz or other double extensions type), you can also use the .net [System.IO.Path]::GetFileNameWithoutExtension($MyFileName) method.
$MyFileName = "Teste.epub"
$FileExt = '.epub'
# Wrong approach
$output = $MyFileName.TrimEnd($FileExt)
write-host $output -ForegroundColor Yellow
#Output returns Test
# Proper method
if ($MyFileName.EndsWith($FileExt)) {
$output = $MyFileName.Substring(0,$MyFileName.Length - $FileExt.Length)
Write-Host $output -ForegroundColor Cyan
}
# Returns Tested
#Alternative method. Won't work if you want to trim out double extensions (eg. tar.gz)
if ($MyFileName.EndsWith($FileExt)) {
$Output = [System.IO.Path]::GetFileNameWithoutExtension($MyFileName)
Write-Host $output -ForegroundColor Cyan
}
You're making this too hard on yourself. Use the .BaseName to get the filename without extension.
Your code simplified:
$SourceFileFolder = 'N:\- Books\- - BMS\- Books Needing Folders'
$MyArray = "*.azw3", "*.cbz", "*.doc", "*.docx", "*.djvu", "*.epub", "*.mobi", "*.mp3", "*.pdf", "*.txt"
(Get-ChildItem -Path $SourceFileFolder -Include $MyArray -File -Recurse) | Sort-Object Name | ForEach-Object {
# BaseName is the filename without extension
$NewDirectory = Join-Path -Path $SourceFileFolder -ChildPath $_.BaseName
$null = New-Item -Path $NewDirectory -ItemType Directory -Force
$_ | Move-Item -Destination $NewDirectory
}

Converting a line of cmd to powershell

EDIT2: Final code below
I need help on converting some codes as I am very new to mkvmerge, powershell and command prompt.
The CMD code is from https://github.com/Serede/mkvtoolnix-batch/blob/master/mkvtoolnix-batch.bat
for %%f in (*.mkv) do %mkvmerge% #options.json -o "mkvmerge_out/%%f" "%%f"
What I've managed so far
$SourceFolder = "C:\tmp" #In my actual code, this is done using folder browser
$SourceFiles = Get-ChildItem -LiteralPath $SourceFolder -File -Include *.mkv
$SourceFiles | foreach
{
start-process "F:\Desktop\#progs\mkvtoolnix\mkvmerge.exe"
}
I'd be grateful for any help as I'm having trouble understanding and converting while learning both sides. Thank you very much.
**EDIT 2:**Here's my final working code.
Function Get-Folder($initialDirectory) {
#Prompt to choose source folder
[void] [System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms')
$FolderBrowserDialog = New-Object System.Windows.Forms.FolderBrowserDialog
$FolderBrowserDialog.Description = 'Choose the video folder'
$FolderBrowserDialog.RootFolder = 'MyComputer'
if ($initialDirectory) { $FolderBrowserDialog.SelectedPath = $initialDirectory }
[void] $FolderBrowserDialog.ShowDialog()
return $FolderBrowserDialog.SelectedPath
}
Function ExitMessage
{
#endregion Function output
Write-Host "`nOperation complete";
Write-Host -NoNewLine 'Press any key to continue...';
$null = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown');
Exit;
}
($SourceFolder = Get-Folder | select )
#Check for output folder and create if unavailable
$TestFile = "$SourceFolder" + "\mkvmerge_out"
if ((Test-Path -LiteralPath $TestFile) -like "False")
{
new-item -Path $SourceFolder -name "mkvmerge_out" -type directory
Write-Host 'Folder created';
}
#Checking for the presence of a Json file
$TestFile = (Get-ChildItem -LiteralPath $SourceFolder -File -Filter *.json)
if ($TestFile.count -eq 0)
{
Write-Host 'json file not found';
ExitMessage;
}
$TestFile = "$SourceFolder" + "\$TestFile"
#Getting the total number of files and start timer.
[Int] $TotalFiles = 0;
[Int] $FilesDone = 0;
$TotalFiles = (Get-ChildItem -LiteralPath $SourceFolder -File -Filter *.mkv).count
$PercentFiles = 0;
$Time = [System.Diagnostics.Stopwatch]::StartNew()
#Start mkvmerge process with progress bar
$mkvmergeExe = 'F:\Desktop\#progs\mkvtoolnix\mkvmerge.exe'
$JsonFile = "$TestFile" # alternatively, use Join-Path
Get-ChildItem -LiteralPath $SourceFolder -File -Filter *.mkv | ForEach-Object {
$PercentFiles = [math]::truncate(($FilesDone/$TotalFiles)*100)
Write-Progress -Activity mkvmerge -Status ("{0}% Completed; {1}/{2} done; Time Elapsed: {3:d2}:{4:d2}:{5:d2}" -f $PercentFiles, $FilesDone, $TotalFiles, $Time.Elapsed.Hours, $Time.Elapsed.minutes, $Time.Elapsed.seconds) -PercentComplete $PercentFiles;
Write-Host "Processing $_"
$f = $_.FullName
$of = "$SourceFolder\mkvmerge_out\$($_.Name)"
& $mkvmergeExe -q `#$JsonFile -o $of $f
$FilesDone++
}
Remove-Item -LiteralPath $JsonFile #Remove this line if you want to keep the Json file
$PercentFiles = [math]::truncate(($FilesDone/$TotalFiles)*100)
Write-Progress -Activity mkvmerge -Status ("{0}% Completed; {1}/{2} done; Time Elapsed: {3:d2}:{4:d2}:{5:d2}" -f $PercentFiles, $FilesDone, $TotalFiles, $Time.Elapsed.Hours, $Time.Elapsed.minutes, $Time.Elapsed.seconds) -PercentComplete $PercentFiles;
ExitMessage;
$mkvmergeExe = 'F:\Desktop\#progs\mkvtoolnix\mkvmerge.exe'
$optionsFile = "$SourceFolder\options.json" # alternatively, use Join-Path
Get-ChildItem -LiteralPath $SourceFolder -File -Filter *.mkv | ForEach-Object {
$f = $_.FullName
$of = "$SourceFolder\mkvmerge_out\$($_.Name)"
& $mkvmergeExe `#$optionsFile -o $of $f
}
Note that your cmd code assumes that it's operating in the current directory, while your PowerShell code passes a directory explicitly via $SourceFolder; therefore, the options.json file must be looked for in $SourceFolder and too, and the output file path passed to -o must be prefixed with $SourceFolder too which is achieved via expandable strings ("...") .
The main points to consider:
for %%f in (*.mkv) has no direct counterpart in PowerShell; you correctly used Get-ChildItem instead, to get a list of matching files, which are returned as System.IO.FileInfo instances.
However, -Include won't work as intended in the absence of -Recurse (unless you append \* - see this GitHub issue; -Filter does, and is also the faster method, but it has its limitations and legacy quirks (see this answer).
While PowerShell too allows you to execute commands whose names or paths are stored in a variable (or specified as a quoted string literal), you then need &, the call operator, to invoke it, for syntactic reasons.
Inside a script block ({ ... }) passed to the ForEach-Object cmdlet, automatic variable $_ represents the pipeline input object at hand.
$_.FullName ensures that the System.IO.FileInfo input instances are represented by their full path when used in a string context.
This extra step is no longer necessary in PowerShell [Core] 6+, where System.IO.FileInfo instances thankfully always stringify as their full paths.
The # character is preceded by ` (backtick), PowerShell's escape character, because # - unlike in cmd - is a metacharacter, i.e. a character with special syntactic meaning. `# ensures that the # is treated verbatim, and therefore passed through to mkvmerge.
Alternatively, you could have quoted the argument instead of escaping just the #: "#$optionsFile"
See this answer for background information.
You generally do not need to enclose arguments in "..." in PowerShell, even if they contain spaces or other metacharacters.

How to run command for several files in PowerShell

I have a folder with images and I need to convert them using ImageMagick . How to run command for each file (as parameter) in Powershell?
For efficiency's sake (since I can see you're on v4 by your use of the array ForEach method), you can use the -File switch on Get-ChildItem (introduced in v3) and only get the files you need. Additionally, using the foreach keyword is more readable and performant than either .ForEach() or ForEach-Object.
You can use the call operator to run external executables (&):
$magick = 'C:\INSTALL\ImageMagick-7.0.8-Q16\magick.exe'
$path = 'C:\Pictres'
foreach ($file in Get-ChildItem -Path $path -File) {
& $magick "$($file.FullName)" -negate "$path\test_$($file.Name)"
}
The Get-ChildItem returns an array of FileInfo objects and you should not treat them as simply string that hold the path and file name.
Instead, use the properties of these objects FullName and Name
Get-ChildItem -Path "C:\Pictres" -File | ForEach-Object {
# The automatic variable '$_' or '$PSItem' contains the current object in the PowerShell pipeline.
$originalImage = $_.FullName
$convertedImage = Join-Path -Path $_.DirectoryName -ChildPath ('test_{0}' -f $_.Name)
& 'C:\INSTALL\ImageMagick-7.0.8-Q16\magick.exe' "$originalImage" -negate "$convertedImage"
}
Use Get-ChildItem and ForEach. $PSItem is defined automatically
(Get-ChildItem -Path "C:\Pictres").ForEach({
>> $BuildName = "C:\Pictres\$PSItem"
>> $BuildName2 = "C:\Pictres\test_$PSItem"
>> C:\INSTALL\ImageMagick-7.0.8-Q16\magick.exe "$BuildName" -negate "$BuildName2"
>> }
>> )

Combine path, file strings and literals for path

Trying to combine a path, filename, and add some text along with a variable for Out-File log.
I've tried many alternatives unsuccessfully and need assistance;
FormattedDate = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$OldVersion = C:\Temp\TestFile.txt
$OldPath = (Get-Item $OldVersion).DirectoryName
$OldBaseName = (Get-Item $OldVersion).BaseName
ErrFile = Join-Path $OldPath OldBaseName
Out-File -FilePath "$ErrFile_$FormattedDate Error.txt"
Out-File -FilePath "$($OldPath)$($OldBaseName)_$($FormattedDate)_Error.txt"
...just two examples.
I've tried many other combinations and driving me crazy.
Basically I want it to be.
C:\Temp\TestFile_2017-08-24 16:51:36_Error.txt
Update:
I've tried both
$filename = '{0}_{1:s}_Error{2}' -f $basename, (Get-Date), $extension
I get _2017-08-25T13:02:17_Error.txt but no basename (TestFile).
$newpath = "${dirname}\${basename}_${date}_Error${extension}"
I get
A drive with the name '_2017-08-25 13' does not exists.
Can you also explain or provide a resource of what '{0}_{1:s}_Error{2}' and/or '{0}_{1:yyyy-MM-dd HH:mm:ss}_Error{2}' does?
Use the format operator (-f) for constructing the filename and Join-Path for building the path.
$oldpath = 'C:\Temp\TestFile.txt'
$basename = [IO.Path]::GetFileNameWithoutExtension($oldpath)
$extension = [IO.Path]::GetExtension($oldpath)
$filename = '{0}_{1:yyyy-MM-dd HH:mm:ss}_Error{2}' -f $basename, (Get-Date), $extension
$newpath = Join-Path ([IO.Path]::GetDirectoryName($oldpath)) $filename
Unless you must have the space in the date format you could simplify the format string by using the standard sortable format specifier (s) that will produce date strings like 2017-08-24T23:58:25 instead of 2017-08-24 23:58:25.
$filename = '{0}_{1:s}_Error{2}' -f $basename, (Get-Date), $extension
If you want to construct the path as a string with inline variables you need to make sure that the underscores in your file name are kept separate from the variable name. Because underscores are valid name components for variable names $var_ is the variable var_, not the variable var followed by a literal underscore. Use curly braces to ensure that variables and literal underscores don't get mixed up.
$oldpath = 'C:\Temp\TestFile.txt'
$date = (Get-Date).ToString('yyyy-MM-dd HH:mm:ss')
$dirname = [IO.Path]::GetDirectoryName($oldpath)
$basename = [IO.Path]::GetFileNameWithoutExtension($oldpath)
$extension = [IO.Path]::GetExtension($oldpath)
$newpath = "${dirname}\${basename}_${date}_Error${extension}"
Addendum: Your file names should not contain colons. Colons in Windows paths either terminate a drive name or separate the file name from the name of an alternate data stream. Your date format string should rather be something like yyyy-MM-dd HH-mm-ss or yyyy-MM-dd_HH-mm-ss to avoid this pitfall.

How to convert absolute path to relative path in PowerShell?

I'd like to convert a path to a relative path in a PowerShell script.
How do I do this using PowerShell?
For example:
Path to convert: c:\documents\mynicefiles\afile.txt
Reference path: c:\documents
Result: mynicefiles\afile.txt
And
Path to convert: c:\documents\myproject1\afile.txt
Reference path: c:\documents\myproject2
Result: ..\myproject1\afile.txt
I found something built in, Resolve-Path:
Resolve-Path -Relative
This returns the path relative to the current location. A simple usage:
$root = "C:\Users\Dave\"
$current = "C:\Users\Dave\Documents\"
$tmp = Get-Location
Set-Location $root
Resolve-Path -relative $current
Set-Location $tmp
Using the built-in System.IO.Path.GetRelativePath is simpler than the accepted answer:
[System.IO.Path]::GetRelativePath($relativeTo, $path)
There is a good answer here, but it changes your current directory (it reverts back), but if you really need to isolate that process, code example below can help. It does the same thing, just in a new PowerShell instance.
function Get-RelativePath($path, $relativeTo) {
$powershell = (Get-Process -PID $PID | Get-Item)
if ([string]::IsNullOrEmpty($powershell)) {
$powershell = "powershell.exe"
}
& $powershell -NoProfile -NonInteractive -ExecutionPolicy Unrestricted -Command "& { Set-Location `"$relativeTo`"; Resolve-Path `"$path`" -Relative}"
}
It's really slow though, you should really use the other version unless you absolutely have to use this.
Sometimes I have multiple "roots" from which I want to generate relative file paths. I have found Resolve-Path -Relative unusable in this kind of situation. Changing a global setting, current location, in order to generate a relative file path seems error-prone and (if you're writing parallel code) possibly not thread-safe.
The following should work in early or recent versions of Powershell and Powershell Core, doesn't change your current directory, even temporarily, and is OS-independent and thread-safe.
It doesn't address the second example from OP (inserting .. as required.)
function Get-RelativePath {
param($path, $relativeTo)
# strip trailing slash
$relativeTo = Join-Path `
(Split-Path -Parent $relativeTo) `
(Split-Path -Leaf $relativeTo)
$relPath = Split-Path -Leaf $path
$path = Split-Path -Parent $path
do {
$leaf = Split-Path -Leaf $path
$relPath = Join-Path $leaf $relPath
$path = Split-Path -Parent $path
} until (($path -eq $relativeTo) -Or ($path.Length -eq 0))
$relPath
}
An example:
PS> $configRoot = 'C:\Users\P799634t\code\RMP\v2\AWD'
PS> $queryPath = 'C:\Users\P799634t\code\RMP\v2\AWD\config_queries\LOAD_UNQ_QUEUE_QUERY2.sql'
PS> Write-Host (Get-RelativePath $queryPath $configRoot)
config_queries\LOAD_UNQ_QUEUE_QUERY2.sql
It behaves reasonably when one file path is not a sub-path of the other:
PS> $root = 'C:\Users\P799634t\code\RMP\v2\AWD'
PS> $notRelated = 'E:\path\to\origami'
PS> Write-Host (Get-RelativePath $notRelated $root)
E:\path\to\origami
A quick and easy way would be :
$current -replace [regex]::Escape($root), '.'
Or if you want the relative path from your actual current location
$path -replace [regex]::Escape((pwd).Path), '.'
This assumes all your paths are valid.
Here is an alternative approach
$pathToConvert1 = "c:\documents\mynicefiles\afile.txt"
$referencePath1 = "c:\documents"
$result1 = $pathToConvert1.Substring($referencePath1.Length + 1)
#$result1: mynicefiles\afile.txt
And
$pathToConvert2 = "c:\documents\myproject1\afile.txt"
#$referencePath2 = "c:\documents\myproject2"
$result2 = "..\myproject" + [regex]::Replace($pathToConvert2 , ".*\d+", '')
#$result2: ..\myproject\afile.txt
Note: in the second case ref path wasn't used.