Search code examples
windowspowershellbatch-filehybrid

PowerShell in Batch hybrid - How to find several different strings in a txt and append to the top only if it does not exist?


This is my attempt at learning PowerShell. No edu background in coding/scripting. Only learned Batch by reading different examples here in StackOverflow and Superuser, trying to adapt found snippets to what is needed, and a lot of trial and error. Please be kind!

There are several strings (without the numbering) that needs to be added to a txt file sequentially and to the top of the file:

  1. [/This/IsJust.AnExample]
  2. IsThisTrue=False
  3. IsThisFalse=

I attempted to "convert" what was found to something that might look usable in a Batchfile:

start "" /wait /min powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "(Get-ChildItem 'D:\Sample Path\%sample_var%\Sample file.txt' -File -recurse | Where{!(Select-String -SimpleMatch '([/This/IsJust.AnExample])|(IsThisTrue=False)|(IsThisFalse=)' -Path $_.fullname -Quiet)} | ForEach{ $Path = $_.FullName '([/This/IsJust.AnExample])|(IsThisTrue=False)|(IsThisFalse=)',(Get-Content $Path) | Set-Content $Path })"

Expectation:

  1. Look for each string. If it doesn't exist, add it
  • The string(s) must only be added if they/it do(es) not exist

Example: If it's entirely missing

[/MoreRandom /Example/In /Here]
MaybeThisIsCorrect=False
MaybeIAmConfused=True
MaybeINeedToLearnTheBasicsOfPowershell=True
MyEnglishIsTerrible=True
Apologetic=True

Expected:

[/This/IsJust.AnExample]
IsThisTrue=False
IsThisFalse=

[/MoreRandom /Example/In /Here]
MaybeThisIsCorrect=False
MaybeIAmConfused=True
MaybeINeedToLearnTheBasicsOfPowershell=True
MyEnglishIsTerrible=True
Apologetic=True
  • The string(s) must be always added to the top of the file in the order they were written in and reordered if they were found in other lines

Example:

<newline/blank>
<last string>
<newline/blank>
<string1>
<newline/blank>
<existing content>

Expected:

<string1>
<string2>
<last string>
<newline/blank>
<existing content>
  • The top of the page must not have any spaces or blanks

Example:

<newline/blank>
<newline/blank>
<newline/blank>
<string1>
<last string>
<string2>
<newline/blank>
<existing content>

Expected:

<string1>
<string2>
<last string>
<newline/blank>
<existing content>
  • The last string added must have a blank after to separate it from the existing content Example:
<last string>
<newline/blank>
<existing content starts here>
  1. All must be inline, no external files such as .ps1, .bat, .txt, etc.
  2. It doesn't need an -include since it's only searching in a specific file.
  3. Must be usable in a batchfile because it's gonna be added to a for loop.
  4. Always able to handle spaces in file paths
  5. Must be able to handle encoding with BOM (some snippets added random non-Latin characters)

Result of original attempt:

  • It just fails without doing anything to the txt.

Result of attempts after receiving suggestions/info:

  • start "" /wait /min powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "(Get-ChildItem 'example.txt' -File -recurse -PipelineVariable FilePath | Where{!(Select-String -SimpleMatch '([/This/IsJust.AnExample])|(IsThisTrue=False)|(IsThisFalse=)' -Path $_.fullname -Quiet)} | ForEach{ [string[]]$ToAdd = [string]::Empty; $Out = Get-Content $FilePath -raw; Switch ($Out){{$_ -notmatch [regex]::Escape('[/This/IsJust.AnExample]')}{$ToAdd += '[/This/IsJust.AnExample]'};{$_ -notmatch 'IsThisTrue=False'}{$ToAdd += 'IsThisTrue=False'};{$_ -notmatch 'IsThisFalse='}{$ToAdd += 'IsThisFalse='}};$ToAdd,$Out | Set-Content -Path $FilePath})" - works but it always adds the missing string on the top of the page not in the order they were written in. It also does not add a blank (separator) in between the last string and the first line of the existing content.
  • '([/This/IsJust.AnExample])|(IsThisTrue=False)|(IsThisFalse=)' - Adds a single line instead of checking each string if it exists
  • ('[/This/IsJust.AnExample]','IsThisTrue=False','IsThisFalse=') - Adds all strings only if all of them of are missing. If there is a single string already present, it won't add the remaining.

Notes:

  • After further digging, the file was found out to be encoded as UTF-16 LE BOM
  • The file paths are always with spaces
  • Added the missing closing parenthesis causing the snippet to crash
  • Changed -NoP -Ex -By -c to -NoProfile -ExecutionPolicy Bypass -Command as suggested by @Compo

What is causing it to fail and how to properly do it?

A link to a dummies guide on how to convert PowerShell to Batch would also be nice.

Additionally, would appreciate to know which part can be reused in order to add more strings if needed in the future.


Solution

  • Mmm... If you want that the solution works in a Batch file, why not write it in a Batch file in first place? Besides, IMHO the Batch solution is simpler than the PowerShell one and it runs much faster than the PS one, specially if you want to run it in a loop...

    EDIT 2023/10/16: Code modified as requested in comments...

    @echo off
    setlocal EnableDelayedExpansion
    
    rem Define the strings that needs to be reviewed
    set "n=0"
    set "strings="
    for %%a in (
       "[/This/IsJust.AnExample]"
       "IsThisTrue=False"
       "IsThisFalse="
       ) do (
       set /A n+=1
       set "string[!n!]=%%~a"
       set "strings=!strings!/C:"%%~a" "
    )
    
    rem Get the line number of the first line of "existing content"
    set "skip="
    for /F "tokens=1* delims=:" %%a in ('findstr /N /V /L %strings% test.txt') do (
       if not defined skip if "%%b" neq "" set /A "skip=%%a-1"
    )
    if defined skip (
       if "%skip%" neq "0" (
          set "skip=skip=%skip%"
       ) else (
          set "skip="
       )
    )
    
    rem Insert the strings at beginning of output file
    (
    for /L %%i in (1,1,%n%) do echo !string[%%i]!
    echo/
    for /F "%skip% delims=" %%a in (test.txt) do echo %%a
    ) > output.txt
    
    rem Last step: update input file
    REM move /Y output.txt test.txt
    

    If you want to run this code in a loop you could directly insert it in the loop, or you can convert it into a subroutine (adding a :label at beginning and an exit /B at end) and call :label in the loop

    PS - You said you need this code run in a for loop, but you don't specify which value will be modified in the loop... If you do so, we could write the complete solution

    EDIT 2023/10/17 New code added because there are A LOT of changes!

    This question have been modified several times with new specifications each time it is modified. Let's review your examples and descriptions so far:

    • In the first example, the input have not anyone of the strings. The expected result is OK.
    • In the second example the input have strings at beginning in different order and then the <existing content>. It does NOT specify that a string could be in the <existing content>
    • In the third example the input have strings and blank lines at beginning and then the <existing content> Again, it does NOT specify that a string could be in the <existing content>
    • The fourth example is even clearer: after the <last string> the result must contain a <newline/blank> and then <existing content starts here> What this "existing content" is? The contents of the input file below the strings (as specified). You never specified that THE EXISTING CONTENT OF THE INPUT FILE COULD INCLUDE A STRING!!! If the "existing content" have a string, then it MUST BE MODIFIED (by removing such a string) when it is passed to the result file. In such a case, it can't be longer called EXISTING CONTENT!!!!! In other words: if you called it "existing content", then you used a term opposite to the desired result (so it is a term that generates confusion, instead of being descriptive)...
    • In your comments you reaffirm the same specifications: In example #3 if D E F exists in the file, insert A B C (space) at beginning. Ok. In example #4 B C D E F exists, that is, the "existing content" is D E F and this content have NOT a string!
    • Finally, in your last comment, you say that my code "keeps adding the strings in the file even if they exist". This can only happen if the strings exists IN THE EXISTING CONTENTS, that is, in any line below the strings that appear at beginning of the file. In other words: if input is D C E B F you want as output A B C (blank) D E F, but my program generates A B C (blank) D C E B F, right? Why you didn't said THAT!

    You have explained A LOT of things, but not the important ones...

    You should help us to help you, not hide important information that makes it difficult to help you... :(

    I hope the new code below solve you problem (at least, the last one)...

    @echo off
    setlocal EnableDelayedExpansion
    
    rem Define the strings that needs to be reviewed
    set "n=0"
    set "strings="
    for %%a in (
       "[/This/IsJust.AnExample]"
       "IsThisTrue=False"
       "IsThisFalse="
       ) do (
       set /A n+=1
       set "string[!n!]=%%~a"
       set "strings=!strings!/C:"%%~a" "
    )
    
    rem Generate the output file:
    (
    rem First, insert the strings at beginning
    for /L %%i in (1,1,%n%) do echo !string[%%i]!
    echo/
    rem Then, insert the lines in the file that have NOT the strings
    for /F "delims=" %%a in ('findstr /V /L %strings% test.txt') do echo %%a
    ) > output.txt
    
    rem Last step: update input file
    REM move /Y output.txt test.txt
    

    LAST CODE: I give up...

    @echo off
    setlocal EnableDelayedExpansion
    
    rem Define the strings that needs to be reviewed
    set "n=0"
    set "strings="
    for %%a in (
       "[/This/IsJust.AnExample]"
       "IsThisTrue=False"
       "IsThisFalse="
       ) do (
       set /A n+=1
       set "string[!n!]=%%~a"
       set "strings=!strings!/C:"%%~a" "
    )
    
    rem Process files with .ini extension
    for %%f in (*.ini) do (
    
       rem Generate the output file:
       (
       rem First, insert the strings at beginning
       for /L %%i in (1,1,%n%) do echo !string[%%i]!
       echo/
       rem Then, insert the lines in the file that have NOT the strings
       rem Convert a UTF-16 LE BOM file to UTF-8 via TYPE command
       for /F "delims=" %%a in ('type "%%f" ^| findstr /V /L %strings%') do echo %%a
       ) > output.txt
    
       rem Last step: update input file
       move /Y output.txt "%%f" > NUL
    
    )