How to convert a Windows CMD Forloop in PowerShell - windows

I have the following code in command shell code.
SET MYdir=%NewPath%\%CUST%\SuppliesTypes
SET "MYsCount=1"
SET /p MYsCount="Number of MYs in project? (default: %MYSCount%): "
for /L %%a in (1,1,%MYsCount%) do (
SET /p MYNums="Enter %%a MY Number: "
call md "%MYdir%\MY_%%MYNums%%"
)
SET "MYsCount="
However, I am converting my code from CMD to PowerShell. I do not fully understand the correct way to convert over. Here might be how it should be done, but it's not working as it just jumps right through.
SET MYdir=%NewPath%\%CUST%\Product
SET "MYsCount=1"
SET /p MYsCount="Number of MYs in project? (default: %MYSCount%): "
For ($MYsCount = 1; $MYsCount -eq 10; $MYsCount++){
SET /p MyNums="Enter %%a Product Numbers: "
CALL MD "%MYdir%\%CUST%\Product_%%"
}
SET "$MYsCount="
I've looked at the following sites and articles:
PowerShell Basics: Programming With Loops (Helped validate)
How to do a forloop in a Django template? (Didn't really help)
Windows PowerShell Cookbook, 3rd Edition (Page 170)
I am running this code inside a While-Loop.
Thanks for your help!

You have an interesting amalgam of batch file and powershell there in your second code block. It is hard to read when some things are one language and some things are another. Let's see if we can't get it all into PowerShell here.
$MYdir = "$NewPath\$CUST\Product"
$MYsCount = 1
$UserMYsCount = Read-Host "Number of MYs in project? (default: $MYSCount): "
If([string]::IsNullOrEmpty($UserMYsCount){
$UserMYsCount = $MYsCount
}
For ($i = 1; $i -le $UserMYsCount; $I++){
$MyNums = Read-Host "Enter $i Product Numbers: "
New-Item -Path "$MYdir\MY_$MyNums" -ItemType Directory
}

I believe the issue is coming from how you are declaring your variables. SET creates the variables as environment variables that powershell does not access natively. Below is how I would write up your section of code:
$MYDir = "$env:NewPath\$env:CUST\SuppliesTypes"
$MYsCount = 1
$MYsCount = read-host -prompt "Number of MYs in project? (default: $MYSCount): "
foreach ($a in 0..$MYsCount){
$MYNums = Read-Host -Prompt "Enter $a Product Numbers: "
New-Item -Path "$MYDir\MY_$MYNums" -ItemType Directory
}
$MYsCount = $null
I used a foreach loop instead of a normal for loop because you were incrementing by one each time and I have noticed a small performance gain from using foreach when the step is not complicated. 0..$variable is a short hand for using each number from 0 to the declared variable.
If you did want to use a for loop as you mentioned then you could use:
For ($MYsCount = 1; $MYsCount -eq 10; $MYsCount++){
as you had expected. This loop will only stop if the $MYsCount variable equals 10 though so if someone set the variable to something above 10 it would run indefinitely.

Related

Powershell IF conditional isn't firing in the way I expected. Unsure what I'm doing wrong

I am writing a simple script that makes use of 7zip's command-line to extract archives within folders and then delete the original archives.
There is a part of my script that isn't behaving how I would expect it to. I can't get my if statement to trigger correctly. Here's a snippet of the code:
if($CurrentRar.Contains(".part1.rar")){
[void] $RarGroup.Add($CurrentRar)
# Value of CurrentRar:
# Factory_Selection_2.part1.rar
$CurrentRarBase = $CurrentRar.TrimEnd(".part1.rar")
# Value: Factory_Selection_2
for ($j = 1; $j -lt $AllRarfiles.Count; $j++){
$NextRar = $AllRarfiles[$j].Name
# Value: Factory_Selection_2.part2.rar
if($NextRar.Contains("$CurrentRarBase.part$j.rar")){
Write-Host "Test Hit" -ForegroundColor Green
# Never fires, and I have no idea why
# [void] $RarGroup.Add($NextRar)
}
}
$RarGroups.Add($RarGroup)
}
if($NextRar.Contains("$CurrentRarBase.part$j.rar")) is the line that I can't get to fire.
If I shorten it to if($NextRar.Contains("$CurrentRarBase.part")), it fires true. But as soon as I add the inline $j it always triggers false. I've tried casting $j to string but it still doesn't work. Am I missing something stupid?
Appreciate any help.
The issue seems to be your for statement and the fact that an array / list is zero-indexed (means they start with 0).
In your case, the index 0 of $AllRarfiles is probably the part1 and your for statement starts with 1, but the file name of index 1 does not contain part1 ($NextRar.Contains("$CurrentRarBase.part$j.rar"), but part2 ($j + 1).
As table comparison
Index / $j
Value
Built string for comparison (with Index)
0
Factory_Selection_2.part1.rar
Factory_Selection_2.part0.rar
1
Factory_Selection_2.part2.rar
Factory_Selection_2.part1.rar
2
Factory_Selection_2.part3.rar
Factory_Selection_2.part2.rar
3
Factory_Selection_2.part4.rar
Factory_Selection_2.part3.rar
Another simpler approach
Since it seems you want to group split RAR files which belong together, you could also use a simpler approach with Group-Object
# collect and group all RAR files.
$rarGroups = Get-ChildItem -LiteralPath 'C:\somewhere\' -Filter '*.rar' | Group-Object -Property { $_.Name -replace '\.part\d+\.rar$' }
# do some stuff afterwards
foreach($rarGroup in $rarGroups){
Write-Verbose -Verbose "Processing RAR group: $($rarGroup.Name)"
foreach($rarFile in $rarGroup.Group) {
Write-Verbose -Verbose "`tCurrent RAR file: $($rarFile.Name)"
# do some stuff per file
}
}

Result to variable in a batch file

I turn in circles and after much research, I seek your expertise on this small case. I can send the result of a PS1 script to a text file, but not to a batch file variable.
the script
Param(
[string]$Fic
)
$EmplacementFichier = [string]
$EmplacementFichier = "$Fic"
$MonFichier = get-content -totalcount 1 $EmplacementFichier
$Resultat = $MonFichier.SubString(92,12)
$RnmFic = "EXANTE_$resultat.REPRESTI.txt"
rename-item $EmplacementFichier -newname $RnmFic
Write-Output $RnmFic
Launched from a batch file:
powershell D:\Rnm-Exante.ps1 -fic "%NOMFIC%" > %Fichier%
It creates a file "%Fichier%" at the location of the script, but does not provide the batch variable.
from batch, you can read the file:
<file.ext set /p "var="`
or you can get the output of your powershell script directly (do not redirect to a file in this case):
for /f "delims=" %%a in ('powershell D:\Rnm-Exante.ps1 -fic "%NOMFIC%"') do set "var=%%a"

Get last n lines or bytes of a huge file in Windows (like Unix's tail). Avoid time consuming options

I need to retrieve the last n lines of huge files (1-4 Gb), in Windows 7.
Due to corporate restrictions, I cannot run any command that is not built-in.
The problem is that all solutions I found appear to read the whole file, so they are extremely slow.
Can this be accomplished, fast?
Notes:
I managed to get the first n lines, fast.
It is ok if I get the last n bytes. (I used this https://stackoverflow.com/a/18936628/2707864 for the first n bytes).
Solutions here Unix tail equivalent command in Windows Powershell did not work.
Using -wait does not make it fast. I do not have -tail (and I do not know if it will work fast).
PS: There are quite a few related questions for head and tail, but not focused on the issue of speed. Therefore, useful or accepted answers there may not be useful here. E.g.,
Windows equivalent of the 'tail' command
CMD.EXE batch script to display last 10 lines from a txt file
Extract N lines from file using single windows command
https://serverfault.com/questions/490841/how-to-display-the-first-n-lines-of-a-command-output-in-windows-the-equivalent
powershell to get the first x MB of a file
https://superuser.com/questions/859870/windows-equivalent-of-the-head-c-command
If you have PowerShell 3 or higher, you can use the -Tail parameter for Get-Content to get the last n lines.
Get-content -tail 5 PATH_TO_FILE;
On a 34MB text file on my local SSD, this returned in 1 millisecond vs. 8.5 seconds for get-content |select -last 5
How about this (reads last 8 bytes for demo):
$fpath = "C:\10GBfile.dat"
$fs = [IO.File]::OpenRead($fpath)
$fs.Seek(-8, 'End') | Out-Null
for ($i = 0; $i -lt 8; $i++)
{
$fs.ReadByte()
}
UPDATE. To interpret bytes as string (but be sure to select correct encoding - here UTF8 is used):
$N = 8
$fpath = "C:\10GBfile.dat"
$fs = [IO.File]::OpenRead($fpath)
$fs.Seek(-$N, [System.IO.SeekOrigin]::End) | Out-Null
$buffer = new-object Byte[] $N
$fs.Read($buffer, 0, $N) | Out-Null
$fs.Close()
[System.Text.Encoding]::UTF8.GetString($buffer)
UPDATE 2. To read last M lines, we'll be reading the file by portions until there are more than M newline char sequences in the result:
$M = 3
$fpath = "C:\10GBfile.dat"
$result = ""
$seq = "`r`n"
$buffer_size = 10
$buffer = new-object Byte[] $buffer_size
$fs = [IO.File]::OpenRead($fpath)
while (([regex]::Matches($result, $seq)).Count -lt $M)
{
$fs.Seek(-($result.Length + $buffer_size), [System.IO.SeekOrigin]::End) | Out-Null
$fs.Read($buffer, 0, $buffer_size) | Out-Null
$result = [System.Text.Encoding]::UTF8.GetString($buffer) + $result
}
$fs.Close()
($result -split $seq) | Select -Last $M
Try playing with bigger $buffer_size - this ideally is equal to expected average line length to make fewer disk operations. Also pay attention to $seq - this could be \r\n or just \n.
This is very dirty code without any error handling and optimizations.
When the file is already opened, it's better to use
Get-Content $fpath -tail 10
because of "exception calling "OpenRead" with "1" argument(s): "The process cannot access the file..."
This is not an answer, but a large comment as reply to sancho.s' answer.
When you want to use small PowerShell scripts from a Batch file, I suggest you to use the method below, that is simpler and allows to keep all the code in the same Batch file:
#PowerShell ^
$fpath = %2; ^
$fs = [IO.File]::OpenRead($fpath); ^
$fs.Seek(-%1, 'End') ^| Out-Null; ^
$mystr = ''; ^
for ($i = 0; $i -lt %1; $i++) ^
{ ^
$mystr = ($mystr) + ([char[]]($fs.ReadByte())); ^
} ^
Write-Host $mystr
%End PowerShell%
With the awesome answer by Aziz Kabyshev, which solves the issue of speed, and with some googling, I ended up using this script
$fpath = $Args[1]
$fs = [IO.File]::OpenRead($fpath)
$fs.Seek(-$Args[0], 'End') | Out-Null
$mystr = ''
for ($i = 0; $i -lt $Args[0]; $i++)
{
$mystr = ($mystr) + ([char[]]($fs.ReadByte()))
}
$fs.Close()
Write-Host $mystr
which I call from a batch file containing
#PowerShell -NoProfile -ExecutionPolicy Bypass -Command "& '.\myscript.ps1' %1 %2"
(thanks to How to run a PowerShell script from a batch file).
Get last n bytes of a file:
set file="C:\Covid.mp4"
set n=7
copy /b %file% tmp
for %i in (tmp) do set /a m=%~zi-%n%
FSUTIL file seteof tmp %m%
fsutil file createnew temp 1
FSUTIL file seteof temp %n%
type temp >> tmp
fc /b tmp %file% | more +1 > temp
REM problem parsing file with byte offsets in hex from fc, to be converted to decimal offsets before output
type nul > tmp
for /f "tokens=1-3 delims=: " %i in (temp) do set /a 0x%i >> tmp & set /p=": " <nul>> tmp & echo %j %k >> tmp
set /a n=%m%+%n%-1
REM output
type nul > temp
for /l %j in (%m%,1,%n%) do (find "%j: "< tmp || echo doh: la 00)>> temp
(for /f "tokens=3" %i in (temp) do set /p=%i <nul) & del tmp & del temp
Tested on Win 10 cmd Surface Laptop 1
Result: 1.43 GB file processed in 10 seconds

Batch script or CMD for daily task on directory content by date

I admit I am a noob when it comes to CMD and bat scripting hence maybe my question has already been answered but I couldn't find it because I am unfamiliar with the terminology.
Basically I am currently running CMD to create a txt file for a directory content, that works fine but I would like to improve this process and started to look into a batch file to run this for multiple directories and by date but only get confused with the commands.
I would really appreciated if you maybe show me the right direction to look up the possible commands. Here is was I am basically trying to achieve:
Scan Directory 1, create log file with all content (filename) with modification of date DDMMYYYY and save under Directory 1 (existing on Desktop)
Repeat above for Directory 2, 3, 4 etc.
Now I am not sure how to approach this and where to start. It looks so simply yet I am have not managed to get to work.
assuming you are on Vista or better and your date format is dd/mm/yyyy:
for /d %%a in ("%userprofile%\Desktop\Directory*") do (
for %%b in ("%%~fa\*") do (
set "fname=%%~fb"
for /f %%c in ("%%~tb") do set "fdate=%%c"
setlocal enabledelayedexpansion
echo !fname! !fdate:/=! >> "%%~fa\LOGFILE.TXT"
endlocal
)
)
First of all your batch script to parse current date-time will be locale specific. As long as you do not plan to use it on non-US Windows it will be fine. My solution was to use simple VBS script to generate current timestamps
So code of my batch file looks like
#echo off
call GetToday.bat
call %TEMP%\SetToday.bat
SET LOGFILE=Log.%TODAY%.log
echo %LOGFILE%
Use your log here
GetToday.bat:
#echo off
set TOOLS_HOME=%~dp0
cscript /NoLogo %TOOLS_HOME%\Today.vbs >%TEMP%\SetToday.bat
call %TEMP%\SetToday.bat
Today.vbs:
Dim d
d = Now
WScript.Echo "SET TODAY=" & Get2Digit(Year(d)) & Get2Digit(Month(d)) & Get2Digit(Day(d))
Function Get2Digit(value)
if 10 > value Then
Get2Digit = "0" & value
Else
Get2Digit = Right(value, 2)
End If
End Function
However given Today.vbs generates today date in form YYMMDD. From my experience such suffixes are much more useful, you could just sort you files by name to find specific date
In PowerShell something like this should work:
$folders = 'C:\path\to\Directory1', 'C:\path\to\Directory2', ...
$refDate = (Get-Date '2013-05-27').Date
$recurse = $false
foreach ($d in $folders) {
$output = Join-Path $d 'filelist.txt'
Get-ChildItem $d -Recurse:$recurse | ? {
-not $_.PSIsContainer -and $_.LastWriteTime.Date -eq $refDate
} | % { $_.Name } | Out-File $output
}
If you want to recurse into the subfolders of the scanned folders you need to change $recurse to $true and perhaps change $_.Name to $_.FullName, so you get the filename with the full path instead of just the filename.

bat file to run sqlplus and then execute command based on result?

Can I have a BAT file run a SQLPlus command, and then based on the result, execute a CMD command, and then another SQLPlus command?
I think it would look something like this
CheckRowCount.SQL file
SELECT COUNT(*) FROM dmsn.ds3r_1xrtt_voice_trigger
BAT file:
sqlplus %USER%/%PASSWORD%#ORACLE #CheckRowCount.SQL
if ROWCOUNT < 1 goto :EX
c:\Autobatch\Spotfire.Dxp.Automation.ClientJobSender.exe http://SERVER/spotfireautomation/JobExecutor.asmx c:\AutoBatch\backup\Trigger_Test.xml
:EX
but I don't think this is exatcly right, ROWCOUNT doens't seem like it would work, also, how would I execute another SQLPlus command after the CMD.
I'm very new to BAT files and SQLPlus
Do yourself a favour and learn Powershell or python. I used to build crazy batch files to automate processes and they were painful.
Powershell is much, much better at building such conditional scripts, especially with the ability to create and monitor sub-threads. Python is great also for this.
The great advantage these bring is you can access the database (and the return results within your script to process and react to the data.
The disadvantage(?) is that you will need to learn one of these languages, but I personally do not see these as disadvantages at all, as they are much better than batch files.
Otherwise, foxidrive's answer should work for you.
Powershell example (kind of long, but the code can be reused):
function CreateConnection
{
param([string]$databaseHost
,[string]$Port
,[string]$SID
,[string]$UserID
,[string]$Password
)
process
{
$ConnectString = "Data Source=(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST={0})(PORT={1}))(CONNECT_DATA=(SID={2})));User Id={3};Password={4};"`
-f $databaseHost,$port,$SID,$UserID,$Password
write-host $connectString
sleep 10
$connection = new-object system.data.oracleclient.oracleconnection( $ConnectString)
return $connection
}
}
$DBMSHost="somehost.somedomain.com"
$DBMSPort=1521
$SID="somesid"
$UserName="user"
$Password="password"
[System.Reflection.Assembly]::LoadWithPartialName('System.Data.OracleClient') | out-null
try {
$Connection = CreateConnection $DBMSHost $DBMSPort $SID $UserName $Password
}
catch
{
write-host ("Could not access Oracle database {0}" -f $_.Exception.ToString())
exit
}
$Query = "SELECT COUNT(*) as rowcount FROM dmsn.ds3r_1xrtt_voice_trigger" // Added name to column to make life easier
$data_set = new-object system.data.dataset
$adapter = new-object system.data.oracleclient.oracledataadapter ($Query, $Connection)
[void] $adapter.Fill($data_set)
$table = new-object system.data.datatable
$table = $data_set.Tables[0] // We now have the actual resukts data
foreach ($row in $table)
{
if ($row.rowcount <1 )
{
$Application = "C:\Autobatch\Spotfire.Dxp.Automation.ClientJobSender.exe"
$Arguments = "http://SERVER/spotfireautomation/JobExecutor.asmx c:\AutoBatch\backup\Trigger_Test.xml"
$CommandLine = "{0} {1}" -f $Application,$Arguments
invoke-expression $CommandLine
}
}
Assuming the sqlplus command returns a simple number then this may work:
#echo off
for /f "delims=" %%a in ('sqlplus %USER%/%PASSWORD%#ORACLE #CheckRowCount.SQL') do set rowcount=%%a
echo.rowcount is set to "%ROWCOUNT%"
if %ROWCOUNT% GTR 0 (
c:\Autobatch\Spotfire.Dxp.Automation.ClientJobSender.exe http://SERVER/spotfireautomation/JobExecutor.asmx c:\AutoBatch\backup\Trigger_Test.xml
)
pause

Resources