I have a powershell script that is started bij a jenskinsfile. This all works well. But in the script I have a function to download a file. This does not work, because the $filePath variable contains the value twice. When this part would be run the log would look like:
https://example.com/api/download
D:\folder\file_2.txt D:\folder\file_2.txt
How can I get the value only to be there ones in $filePath ?
function DownloadFile ($folder, $version) {
$wc = New-Object System.Net.WebClient
$wc.Headers['Content-Type'] = 'application/json'
$requesturl = "https://example.com/api/download"
$filePath = Join-Path -Path $folder -ChildPath "\file_$version.txt"
Write-Host "$requesturl"
Write-Host "$filePath"
$wc.DownloadFile($requesturl, $filePath)
return $filePath
}
The implication is that the argument you're passing to the -folder parameter (as represented inside your function as the $folder parameter variable) is an array of folder paths, not a single one.
The solution is therefore to make sure that you only pass a single folder path when you call your DownloadFile function; e.g.:
DownloadFile -folder D:\folder -version 2
# With *positional* parameter binding:
DownloadFile D:\folder 2
Since Join-Path accepts an array of paths as a -Path argument, it outputs multiple paths when given an array; e.g.:
PS> $folder = 'c:\abc', 'c:\def'; Join-Path -Path $folder -ChildPath file.txt
c:\abc\file.txt
c:\def\file.txt
Passing an array Write-Host implicitly stringifies it, which means creating a single string composed of the array elements joined with spaces:
PS> $folder = 'c:\abc', 'c:\def'; Write-Host $folder
c:\abc c:\def
(Note that this differs from implicit output / output via Write-Output, which prints each array element on its own line; also, implicit output / Write-Output write to the pipeline, meaning they output data for later processing, whereas Write-Host writes strings to the display).
Related
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.
I want to make a dynamic function that searches for the requested $ErrorCode within the files inputted and eventually copy the files with the error to another folder.
Right now, my code takes only one file and returns the sentence of where the $Error_Code was found. I want to search through multiple files and return the name of the file that have the $ErrorCode.
function SearchError{
Param (
[Parameter (Mandatory=$true)] [STRING] $SourcePath,
[Parameter (Mandatory=$true)] [STRING] $SourceFile,
[Parameter (Mandatory=$true)] [STRING] $ErrorCode,
[Parameter (Mandatory=$true)] [STRING] $FileType
# [Parameter (Mandatory=$true)] [STRING] $DestPath
)
$TargetPath = "$($SourcePath)\$($SourceFile)"
#Return $TargetPath
$DestinationPath = "$($DestPath)"
#Return $DestinationPath
#foreach($error in $TargetPath) {
Get-ChildItem $TargetPath | Select-String -pattern $ErrorCode
}
SearchError
Select-String's output objects - which are of type [Microsoft.PowerShell.Commands.MatchInfo] - have a .Path property that reflects the input file path.
Adding the -List switch to Select-String makes it stop searching after the first match in the file, so you'll get exactly 1 output object for each file in which at least 1 match was found.
Therefore, the following outputs only the paths of the input files in which at least 1 match was found:
Get-ChildItem $TargetPath |
Select-String -List -Pattern $ErrorCode | ForEach-Object Path
Note: -Pattern supports an array of regex patterns, so if you define your $ErrorCode parameter as [string[]], files that have any one of the patterns will match; use -SimpleMatch instead of -Pattern to search by literal substrings instead.
Re:
eventually copy the files with the error to another folder
Simply appending | Copy-Item -Destination $DestPath to the above command should do.
Re:
I want to search through multiple files
Depending on your needs, you can make your $SourcePath and $SourceFile parameters array-valued ([string[]]) and / or pass wildcard expressions as arguments.
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"
>> }
>> )
If I want to combine two strings into a file path, I use Join-Path like this:
$path = Join-Path C: "Program Files"
Write-Host $path
That prints "C:\Program Files". If I want to do this for more than two strings though:
$path = Join-Path C: "Program Files" "Microsoft Office"
Write-Host $path
PowerShell throws an error:
Join-Path : A positional parameter cannot be found that accepts argument 'Microsoft Office'.
At D:\users\ma\my_script.ps1:1 char:18
+ $path = join-path <<<< C: "Program Files" "Microsoft Office"
+ CategoryInfo : InvalidArgument: (:) [Join-Path], ParameterBindingException
+ FullyQualifiedErrorId : PositionalParameterNotFound,Microsoft.PowerShell
.Commands.JoinPathCommand
I tried using a string array:
[string[]] $pieces = "C:", "Program Files", "Microsoft Office"
$path = Join-Path $pieces
Write-Host $path
But PowerShell prompts me to enter the childpath (since I didn't specify the -childpath argument), e.g. "somepath", and then creates three files paths,
C:\somepath
Program Files\somepath
Microsoft Office\somepath
which isn't right either.
You can use the .NET Path class:
[IO.Path]::Combine('C:\', 'Foo', 'Bar')
Since Join-Path can be piped a path value, you can pipe multiple Join-Path statements together:
Join-Path "C:" -ChildPath "Windows" | Join-Path -ChildPath "system32" | Join-Path -ChildPath "drivers"
It's not as terse as you would probably like it to be, but it's fully PowerShell and is relatively easy to read.
Since PowerShell 6.0, Join-Path has a new parameter called -AdditionalChildPath and can combine multiple parts of a path out-of-the-box. Either by providing the extra parameter or by just supplying a list of elements.
Example from the documentation:
Join-Path a b c d e f g
a\b\c\d\e\f\g
So in PowerShell 6.0 and above your variant
$path = Join-Path C: "Program Files" "Microsoft Office"
works as expected!
Join-Path is not exactly what you are looking for. It has multiple uses but not the one you are looking for. An example from Partying with Join-Path:
Join-Path C:\hello,d:\goodbye,e:\hola,f:\adios world
C:\hello\world
d:\goodbye\world
e:\hola\world
f:\adios\world
You see that it accepts an array of strings, and it concatenates the child string to each creating full paths. In your example, $path = join-path C: "Program Files" "Microsoft Office". You are getting the error since you are passing three positional arguments and join-path only accepts two. What you are looking for is a -join, and I could see this being a misunderstanding. Consider instead this with your example:
"C:","Program Files","Microsoft Office" -join "\"
-Join takes the array of items and concatenates them with \ into a single string.
C:\Program Files\Microsoft Office
Minor attempt at a salvage
Yes, I will agree that this answer is better, but mine could still work. Comments suggest there could be an issue with slashes, so to keep with my concatenation approach you could do this as well.
"C:","\\Program Files\","Microsoft Office\" -join "\" -replace "(?!^\\)\\{2,}","\"
So if there are issues with extra slashes it could be handled as long as they are not in the beginning of the string (allows UNC paths). [io.path]::combine('c:\', 'foo', '\bar\') would not work as expected and mine would account for that. Both require proper strings for input as you cannot account for all scenarios. Consider both approaches, but, yes, the other higher-rated answer is more terse, and I didn't even know it existed.
Also, would like to point out, my answer explains how what the OP doing was wrong on top of providing a suggestion to address the core problem.
If you are still using .NET 2.0, then [IO.Path]::Combine won't have the params string[] overload which you need to join more than two parts, and you'll see the error Cannot find an overload for "Combine" and the argument count: "3".
Slightly less elegant, but a pure PowerShell solution is to manually aggregate path parts:
Join-Path C: (Join-Path "Program Files" "Microsoft Office")
or
Join-Path (Join-Path C: "Program Files") "Microsoft Office"
Here are two more ways to write a pure PowerShell function to join an arbitrary number of components into a path.
This first function uses a single array to store all of the components and then a foreach loop to combine them:
function Join-Paths {
Param(
[Parameter(mandatory)]
[String[]]
$Paths
)
$output = $Paths[0]
foreach($path in $Paths[1..$Paths.Count]) {
$output = Join-Path $output -ChildPath $path
}
$output
}
Because the path components are elements in an array and all part of a single argument, they must be separated by commas. Usage is as follows:
PS C:\> Join-Paths 'C:', 'Program Files', 'Microsoft Office'
C:\Program Files\Microsoft Office
A more minimalist way to write this function is to use the built-in $args variable, and then collapse the foreach loop into a single line using Mike Fair's method.
function Join-Paths2 {
$path = $args[0]
$args[1..$args.Count] | %{ $path = Join-Path $path $_ }
$path
}
Unlike the previous version of the function, each path component is a separate argument, so only a space is necessary to separate the arguments:
PS C:\> Join-Paths2 'C:' 'Program Files' 'Microsoft Office'
C:\Program Files\Microsoft Office
Here's something that will do what you'd want when using a string array for the ChildPath.
$path = "C:"
#( "Program Files", "Microsoft Office" ) | %{ $path = Join-Path $path $_ }
Write-Host $path
Which outputs
C:\Program Files\Microsoft Office
The only caveat I found is that the initial value for $path must have a value (cannot be null or empty).
The following approach is more concise than piping Join-Path statements:
$p = "a"; "b", "c", "d" | ForEach-Object -Process { $p = Join-Path $p $_ }
$p then holds the concatenated path 'a\b\c\d'.
(I just noticed that this is the exact same approach as Mike Fair's, sorry.)
You can use it this way:
$root = 'C:'
$folder1 = 'Program Files (x86)'
$folder2 = 'Microsoft.NET'
if (-Not(Test-Path $(Join-Path $root -ChildPath $folder1 | Join-Path -ChildPath $folder2)))
{
"Folder does not exist"
}
else
{
"Folder exist"
}
Or you could write your own function for it (which is what I ended up doing).
function Join-Path-Recursively($PathParts) {
$NumberOfPathParts = $PathParts.Length;
if ($NumberOfPathParts -eq 0) {
return $null
} elseif ($NumberOfPathParts -eq 1) {
return $PathParts[0]
} else {
return Join-Path -Path $PathParts[0] -ChildPath $(Join-Path-Recursively -PathParts $PathParts[1..($NumberOfPathParts-1)])
}
}
You could then call the function like this:
Join-Path-Recursively -PathParts #("C:", "Program Files", "Microsoft Office")
Join-Path-Recursively #("C:", "Program Files", "Microsoft Office")
This has the advantage of having the exact same behaviour as the normal Join-Path function and not depending on the .NET Framework.
Pretty new to Powershell and hoping someone can point me in the right direction. Im trying to figure out if there is a cleaner way to accomplish what I have below? Is there a way to refresh to contents of Get-ChildItem once I have made some changes to the files which are returned during the first Get-ChildItem call (stored in $items variable)?
During the first foreach statement I am creating a log signature for all the files that are returned. Once that is done, what I need to do is; get a listing once again (because the item in the path have changed), the second Get-ChildItem will include both the files that were found during the first Get-ChildItem call and also all the logFiles that were generated when the first foreach statement called the generate-LogFile function. So my question, is there a way to update the listing without having to call get-chilItem twice, as well as use two foreach statements?
Thanks for all the help!
--------------This is what I changed the code based on recommendation--------------
$dataStorePath = "C:\Process"
function print-All($file)
{
Write-Host "PrintALL filename:" $file.FullName #Only prints when print-All($item) is called
}
function generate-LogFile($file)
{
$logName = $file.FullName + ".log"
$logFilehandle = New-Object System.IO.StreamWriter $logName
$logFilehandle.Writeline($logName)
$logFilehandle.Close()
return $logName
}
$items = Get-ChildItem -Path $dataStorePath
foreach ($item in $items)
{
$log = generate-LogFile($item) #Contains full path C:\Process\$fileName.log
print-All($item)
print-All($log) #When this goes to the function, nothing prints when using $file.FullName in the print-All function
}
---------Output--------------
For testing I have two files in C:\Process:
fileA.txt & fileB.txt
I will create two additional files
fileA.txt.log & fileB.txt.log
Eventually I need to do something with all four files. I created a print-All Function where I would process all four files. Below is the current ouput. As can be seen, I only get output for the two original files found, not the two additional created (get blank lines when calling the print-All($log)). I need to able to use fullpath property provided by Get-ChildItem, thus using FullName
PrintALL filename: fileA.txt
PrintALL filename:
PrintALL filename: fileB.txt
PrintALL filename:
I'm not entirely clear on what you are asking, by can have generate-LogFile return the created log file, then just call generateRequestData on both your file and the log file? Something like this:
$items = Get-ChildItem -Path $dataStorePath
foreach ($file in $items)
{
$logFile = generate-LogFile $file
generateRequestData $file
generateRequestData $logFile
}
Edit:
In your added sample, you are returning a string from generate-LogFile. .NET strings don't have a FullName property, so nothing gets printed in print-All. To get the FileInfo object that you want, use the get-item commandlet:
return Get-Item $logName
Also, in this example, you don't need to use a StreamWriter to write to the file, you could use the native powershell Out-File commandlet:
function generate-LogFile($file)
{
$logName = $file.FullName + ".log"
$logName | Out-File $logName
return Get-Item $logName
}