Grep | sed -i equivalent for Windows - bash

I'm trying to replicate this command into PowerShell:
grep -lR 'Find' ./src | while read filename; do sed -i".bak" 's/Find/Replace/g' "$filename"; done
What I have so far:
Get-ChildItem "src" -Recurse -File | ForEach-Object { $f = $_; (Get-Content $f) | ForEach-Object { $_ -replace "Find", "Replace" } | Set-Content "$f.tmp"; Move-Item "$f.tmp" $f -Force }
I'm getting an error saying "filename.tmp" does not exist. I thought the above would create the file while parsing. Any help would be appreciated.

Most likely you've fallen victim to Windows PowerShell's inconsistent stringification of the System.IO.FileInfo instances output by Get-ChildItem - see this answer.
The workaround is to use explicit stringification via the .FullName property, which explicitly returns an item's full path.
Applied to your command, alongside some optimizations:
Get-ChildItem -File -Recurse src | ForEach-Object {
$f = $_.FullName # !! Explicitly retrieve the full path
(Get-Content $f -Raw) -creplace 'Find', 'Replace' |
Set-Content -NoNewline "$f.tmp"
Move-Item "$f.tmp" $f -Force
}
Get-Content -Raw reads the entire file into memory as a single string, which is more efficient.
-creplace (which performs case-sensitive replacement, as sed would by default) is directly applied to the resulting multiline string and replaces all occurrences.
-NoNewline (PSv5+) ensures that Set-Content doesn't add an additional trailing newline to the multiline string being saved (the same applies to Out-File / >).
Note: Given that Get-Content -Raw reads the entire file up front, you could even write the modified content back to the very same file, without requiring an intermediate temporary file and a subsequent Move-Item call; that said, doing so bears a slight risk of data loss, if the process of writing back to the same file is interrupted.
Also, while your sed call retains the original file with extension .bak, your PowerShell command does not.

Related

Using PowerShell only copy folders (and contents within) that don't contain a string

I have a long list of folders. Most of the folders follow the "name_#name" format. I have some that don't follow that structure. I want to move all the folders (and the sub-folders/files within) that DON'T have "_" in the folder name.
For example:
test_#12352
moose_#4532
horse_#84462
cow24
fish3
Moved:
cow24
fish3
I think Move files when they contain a specific word? could be modified to make it work...just not sure how. I'm used to just using GUI, this is my first time using PowerShell
When I tried using that code in that link it didn't work with my situation
What you want to do is just filter the list before you move any files
So you can use the following to pick up all the files you want
$Files = Get-childItem -Path $Path -File
You can then filter it down. My favourite way is to pipe the variable into Where-Object and play around with the individual properties and match types. Since you don't want to include the _ we can use a -notmatch "_" to exclude those values
$Files = Get-ChildItem -Path $Path -File | Where-object{$_.Name -notmatch "_"}
And finally, you can move the files
$Files | move-item -path $_.FullName -Destination $Destination
Or as a one liner
Get-ChildItem -Path $Path -File | Where-object{$_.Name -notmatch "_"} | move-item -path $_.FullName -Destination $Destination
*Please note I haven't really tested this code. So test it out yourself before you run it

Piping in PowerShell: Connot bind argument to parameter <parameter> because it is null

I have been practicing PowerShell by dealing with some of the tasks I could do in the file explorer. I am organizing some files for a python project which I am doing. My goal was to copy all python files in the current directory into the "V0.0_noProgressBar" directory:
ls -Filter "*.py" | copy $_ "V0.0_noProgressBar"
but it fails:
Cannot bind argument to parameter 'Path' because it is null.
At line:1 char:26
+ ls -filter "*.py" | copy $_ "V0.0_noProgressBar"
+ ~~
+ CategoryInfo : InvalidData: (:) [Copy-Item], ParameterBindingValidationException
+ FullyQualifiedErrorId : ParameterArgumentValidationErrorNullNotAllowed,
Microsoft.PowerShell.Commands.CopyItemCommand
I assume this should be sufficient information to figure this out, let me know if more is needed. I have run into similar issues a number of times, so there must be a fundamental problem with my understanding of the placeholder $_.
This can be simplified to:
Copy-Item *.py V0.0_noProgressBar
To answer original question, why $_ is not working:
$_ is only valid in script contexts, not just anywhere in the pipeline. E. g. you could use it in a script block of ForEach-Object:
Get-ChildItem -filter "*.py" | ForEach-Object { Copy-Item $_ "V0.0_noProgressBar" }
To augment zett42's succinct answer. $_ makes an appearance in several areas of PowerShell. Some cmdlets allow it's use in a script block the output of which is treated as the argument to the parameter. In keeping with the question the *-Item cmdlets can make use of $_.
Get-ChildItem -Filter "*.txt" | Copy-Item -Destination { "C:\"+ $_.Name }
Obviously that's just an example. Perhaps a more useful case is Rename-Item -NewName { ... $_ ... }. -NewName which also works this way.
Other common cmdlets that make use of $_ are Select-Object, Sort-Object, & Group-Object. Overlapping some of these $_ is used by many cmdlets to help define calculated properties. I strongly recommend reading this about topic. Along with the use of $_ calculated properties are extremely useful. I use them with Select-Object everyday!
If you really want to pass a collection of files through a pipeline,
you can do this:
ls -Filter *.py | Copy-Item -Destination "V0.0-NoProgressBar"
Here the Copy-Item cmdlet has no -Path parameter, so it gets the files from the pipeline. See Help Copy-Item -full to see that the -Path parameter can accept pipeline input.
This is not as simple as answers already given, but it does show an alternative to trying to use $_ in a context where it is unavailable.

batch renaming files using powershell

I am able to batch rename files in a working directory by using:
Dir | %{Rename-Item $_ -NewName ("0{0}.wav" -f $nr++)}
However I want the file rename to start at something other than zero. Say 0500, and rename sequentially in order.
Dir | %{Rename-Item $_ -NewName ("0{500}.wav" -f $nr++)}
returns error.
How can I tell rename to start at a number other than 0?
You can initialize the counter beforehand to 500. Also, you don't need to use a ForEach-Object loop (%) for this, because the NewName parameter can take a scriptblock.
Important here is that you need to put the Get-ChildItem part in between brackets to let that finish before renaming the items, otherwise you may end up trying to rename files that have already been renamed.
$nr = 500
(Get-ChildItem -Path 'D:\Test' -Filter '*.wav' -File) | Rename-Item -NewName { '{0:D4}{1}' -f ($script:nr++), $_.Extension }

Sort text file by portion of filename into folder

I work for a law firm and we have a process that spits out text files with specific information on them in a specific pattern.
Let's say it's this:
1111.xxxxx_2222
I need to create a folder based on the characters that match the 'x' section and move the text files that have the same matching characters in the 'x' section into that folder, repeatedly. There's a couple hundred we have to go through a day so it gets a little tedious doing them by hand.
I've tried this:
$Source = 'Source folder'
(Get-ChildItem -Path $Source -File) | ForEach-Object {
$destination = Join-Path -Path $Source -ChildPath $_.BaseName
if(!(Test-Path $destination)){
New-Item -ItemType Directory -Path $destination | Out-Null
}
$_ | Move-Item -Destination $destination
}
I left the source folder off because it is a network location that I can edit.
This script is not specific enough. It does sort them based on their entire name and it only moves them to a folder of that same name. It cannot run again because the name may be created already which will not work in my case. I'm VERY NEW to power shell and can code at a basic level so any help is appreciated!
You can use a delay-bind script block:
$Source = 'Source folder'
Get-ChildItem -Path $Source -File | Move-Item -WhatIf -Destination {
# Derive the target dir. from the input filename.
$dirPath = Join-Path $Source ($_.BaseName -split '[._]')[1]
# Make sure that the target dir. exists. Note that the
# -Force switch ensures that no error occurs if the dir. already exists.
$null = New-Item -Type Directory -Force $dirPath
$dirPath # Output the target directory path.
}
Note the use of the -WhatIf common parameter, which previews the move operations, but note that only the moving of the files is previewed; the creation of the target directories still happens, because the delay-bind script block must be run even with WhatIf present, so as to be able to display the would-be destination path.
Once the preview signals that the moving would work as intended, remove -WhatIf from the Move-Item call.
($_.BaseName -split '[._]')[1] is what extracts the xxxxx from a file (base) name such as 111.xxxxx_2222, using -split, the regex-based string splitting operator

script to convert all .txt file content to lowercase

I have about 700 .txt files scattered in 300 directories and sub-directories.
I would like to open each of them, convert all text inside to lowercase, including Unicode characters (such as É to é), then save and close them.
Can you advise how it could be done through PowerShell? It is my own computer and I have admin rights.
I have started with the below:
Get-ChildItem C:\tmp -Recurse -File | ForEach-Object {}
but I am not sure what to put between the brackets of ForEach-Object {}.
Simple script, with your requirements:
$path=".\test\*.txt"
#With Default system encoding
Get-ChildItem $path -Recurse | foreach{
(Get-Content $_.FullName).ToLower() | Out-File $_.FullName
}
#Or with specified encoding
Get-ChildItem $path -Recurse | foreach{
(Get-Content $_.FullName -Encoding Unicode).ToLower() |
Out-File $_.FullName -Encoding Unicode
}
#Test
Get-ChildItem $path -Recurse | foreach{
Write-Host "`n File ($_.FullName): `n" -ForegroundColor DarkGreen
Get-Content $_.FullName
}
You need to use :
# Reading the file content and converting it to lowercase and finally putting the content back to the file with the same filename.
(Get-Content C:\path\file.txt -Raw).ToLower() | Out-File C:\path\file.txt -Force
inside the foreach and then change the case to lower.
If you want to iterate all the files in the corresponding folder then you can use another foreach to get that job done.
Hope it helps.

Resources