2

I'm new to the batch editing in Windows, and I'm getting very confused by the impossibility to use PowerShell commands inside my bat file.

Long story short I have this input file:

The quick brown fox jumped over the lazy white dog
The quick red fox jumped over the lazy dog
The quick green wolf jumped over the lazy brown dog
The quick brown fox jumped over the lazy dog
The quick red lion jumped over the lazy dog
The quick green pig jumped over the lazy brown dog
The quick brown fox jumped over the lazy dog

I would like to substitute brown with white, if the sentence is containing the term fox.

As an extra requirement, I would like to replace brown only if it's the first occurrence.

I tried to work with .contains and with find, but still it all get's messy when I try to use the outcome of the if condition.

The best I managed to put together was this; but it's still crap.

@echo on &setlocal
set "search=brown"
set "replace=white"
set "textfile=C:\JavaLogs\ReplaceDemo.txt"
set "newfile=C:\JavaLogs\Output.txt"
set lines=0
(
for /f "delims=" %%i in (%textfile%) do (
    set lines=echo %%i | findstr /C:"fox" |  measure -w -c -l
    echo %lines%
    if "%lines%">"0" echo "There is a fox"
    if "%lines%"=="0" echo "There is no fox"
    set %%i=%%i:%search%=%replace% echo(%%i>%newfile%)  

    else echo(!%%i!)>"%newfile%"
    ) 
)

The final resut would be an output file like this:

The quick white fox jumped over the lazy white dog
The quick red fox jumped over the lazy dog
The quick green wolf jumped over the lazy brown dog
The quick white fox jumped over the lazy dog
The quick red lion jumped over the lazy dog
The quick green pig jumped over the lazy brown dog
The quick white fox jumped over the lazy dog
Compo
  • 36,585
  • 5
  • 27
  • 39
  • 3
    In a batch file, you cannot directly use PowerShell commands such as `measure` (`Measure-Object`) - you'd have to call via Windows PowerShell's / PowerShell (Core)'s CLI, `powershell.exe` / `pwsh.exe`, which comes with syntax challenges. Unless there's a good reason to keep using batch files (`.cmd`, `.bat`), switching to PowerShell and its `.ps1` files, in which you can use PowerShell's vastly superior scripting language, is the way to go. – mklement0 Sep 01 '23 at 22:58
  • What do you mean by the first occurrence of brown? In the line "The quick green pig jumped over the lazy brown dog", You didn't replace the first occurrence of brown. (which is towards the end of the line) – Tolga Sep 01 '23 at 23:17
  • Can you please clarify whether each of `brown` and `fox` are preceded and succeeded by nothing and/or whitespace only. You used "term" for the latter, which to me allows each to be preceded and succeeded by any character, including punctuation; e.g. `unbrowned`, `outfoxed` or `brown,`. – Compo Sep 02 '23 at 10:53

5 Answers5

2

I would recommend using PowerShell, especially if you are new and learning scripting under Windows.

Example of what you want to do under PowerShell is create a MyReplace.ps1 file with this content:

param (
    [Parameter(Mandatory=$true)]
    [string]$Text
)
if ($Text -match 'fox') {
    $Text -replace '(?=brown)brown(.*)', 'white$1'
}
else { 
    $Text
}

Then from a PowerShell console window you can run:

Get-Content C:\JavaLogs\ReplaceDemo.txt | % { .\MyReplace.ps1 $_ } 

This will run the logic you want and output your text.

Finally if you want to send the output to another file you would change the above to:

Get-Content C:\JavaLogs\ReplaceDemo.txt | % { .\MyReplace.ps1 $_ } |  Set-Content C:\JavaLogs\Output.txt
Tolga
  • 2,643
  • 1
  • 27
  • 18
  • Useful solution I couldn't use it because of a limitation on my system: File C:\JavaLogs\MyReplace.ps1 cannot be loaded because running scripts is disabled on this system. – Roberto Appa Sep 02 '23 at 11:21
1
  • You cannot directly call PowerShell commands such as measure (Measure-Object) from a batch file, you need to call via PowerShell's CLI (powershell.exe for Windows PowerShell, pwsh.exe for PowerShell (Core) 7+).

  • Specifically, you can use the -Command (-c) parameter (implied for powershell.exe only) to pass arbitrary PowerShell code to a PowerShell CLI, as shown below.

    • As an aside:

      • Given PowerShell's vastly superior scripting language, consider coding your entire task via a PowerShell script file, .ps1, which you can invoke directly from PowerShell, and also via the CLI using the -File parameter.

      • PowerShell script-file execution is disabled by default on workstation editions of Windows, and requires one-time configuration to enable it, typically with Set-ExecutionPolicy -Force -Scope CurrentUser RemoteSigned - see this answer for details. When using the CLI with -File, you may optionally bypass the effective execution policy ad hoc with -ExecutionPolicy Bypass (e.g., powershell -ExecutionPolicy Bypss -File someScript.ps1)

        • However, if the effective execution policy prevents script execution via GPOs (Group Policy Objects), the only way to enable script execution is by changing these policies.

The following replaces your entire for loop with a single call to powershell.exe:

@echo off & setlocal
set "search=brown"
set "replace=white"
set "textfile=C:\JavaLogs\ReplaceDemo.txt"
set "newfile=C:\JavaLogs\Output.txt"

type "%textFile%" | powershell -c "$input | foreach { if ($_ -match 'fox') { $_ -replace '%search%(.*)$', '%replace%$1' } else { $_ } }" > "%newfile%"
  • The automatic $input variable represents all pipeline (stdin) input (provided via cmd.exe's internal type command in this case), line by line.

  • foreach (ForEach-Object) processes each input line, reflected in the automatic $_ variable:

    • The regex-based -match operator is used to detect the presence of substring fox on each line.

    • If found, the also regex-based -replace operator is used to replace the (first only) match of the search string in the line at hand with the replacement string.

      • For an explanation of the regex (which expands to brown(.*)$) and substitution expression (which expands to white$1) and the option to experiment with them, see this regex101.com page.
    • Otherwise, the line is passed through as-is.

mklement0
  • 382,024
  • 64
  • 607
  • 775
0
@ECHO OFF
SETLOCAL ENABLEDELAYEDEXPANSION
rem The following settings for the directories and filenames are names
rem that I use for testing and deliberately includes spaces to make sure
rem that the process works using such names. These will need to be changed to suit your situation.

SET "sourcedir=u:\your files"
SET "destdir=u:\your results"
SET "filename1=%sourcedir%\q77026292.txt"
SET "outfile=%destdir%\outfile.txt"

SET "prerequisite=fox"
SET "replace=brown"
SET "replacement=white"

(
FOR /f "usebackqdelims=" %%e IN ("%filename1%") DO (
 rem line is in %%e
 SET "foundpre="
 FOR %%o IN (%%e) DO IF "%%o"=="%prerequisite%" SET "foundpre=y"
 IF DEFINED foundpre (
  SET "outline="
  SET "replace1=y"
  FOR %%o IN (%%e) DO IF DEFINED replace1 (
   IF "%%o"=="%replace%" (
    SET "replace1="
    SET "outline=!outline! %replacement%"
   ) ELSE SET "outline=!outline! %%o"
  ) ELSE SET "outline=!outline! %%o"
  ECHO !outline:~1!
 ) ELSE ECHO %%e
)
)>"%outfile%"

GOTO :EOF

Note that if the filename does not contain separators like spaces, then both usebackq and the quotes around %filename1% can be omitted.

Note the use of delayedexpansion Stephan's DELAYEDEXPANSION link as the value of outline is changing in the for %%e loop, and of Boolean variables which are interpreted on their run-time state.

Magoo
  • 77,302
  • 8
  • 62
  • 84
0

If you -must- have a .bat file script, you can run PowerShell from it. Note that this uses PowerShell Core, the current version of PowerShell. The 5.1 version is 1/2 decade old. If you -must- use Windows PowerShell, change pwsh.exe to powershell.exe.

@ECHO OFF
set "search=brown"
set "replace=white"
set "textfile=C:\src\ttt\ReplaceDemo.txt"
set "newfile=C:\src\ttt\ReplaceDemoOutput.txt"

pwsh.exe -NoLogo -NoProfile -Command ^
    Get-Content -Path "%textfile%" ^| ^
        ForEach-Object { $_ -replace '(?=%search%)%search%(\s+fox\s+.*)', '%replace%$1'} ^| ^
        Out-File -FilePath "%newfile%"
EXIT /B

You should get your machine set to where it can run PowerShell scripts. If this is not permitted by the organization, I would question my future with the organization.

Set-ExecutionPolicy -ExecutionPolicy RemoteSigned

A simple, hardcoded PowerShell script to do this might be:

$Search = 'brown'
$Replace = 'white'
$TextFile = 'C:\src\ttt\ReplaceDemo.txt'
$NewFile = 'C:\src\ttt\ReplaceDemoOutput.txt'

Get-Content -Path $TextFile |
    ForEach-Object { $_ -replace "$Search(\s+fox\s+.*)", "$Replace`$1" } |
    Out-File -FilePath $NewFile

If you want a fully parameterized command, you might use something like this. Perhaps the subsequent term, 'fox', also needs to be a parameter.

<#
.Synopsis
    Replaces text.
.Description
    Replaces text.
.Notes
    #Requires -Version 5.1
    https://stackoverflow.com/q/77026292/447901
.Parameter Search
    String to replace when followed by 'fox'.
.Parameter Replace
    String with which to replace the Search string.
.Parameter TextFile
    Specifies the input file.
.Parameter NewFile
    Specifies the output file.
.Example
    ReplaceDemo-2.ps1 -Search 'brown' -Replace 'white' -TextFile '.\ReplaceDemo.txt' -NewFile '.\ReplaceDemoOutput.txt'
.Example
    ReplaceDemo-2.ps1 'brown' 'white' '.\ReplaceDemo.txt' '.\ReplaceDemoOutput.txt'
#>
[CmdletBinding()]
param (
    [Parameter(Mandatory=$true,Position=0)] [string] $Search
    ,[Parameter(Mandatory=$true,Position=1)] [string] $Replace
    ,[Parameter(Mandatory=$true,Position=2)] [string] $TextFile
    ,[Parameter(Mandatory=$true,Position=3)] [string] $NewFile
)
Get-Content -Path $TextFile |
    ForEach-Object { $_ -replace "$Search(\s+fox\s+.*)", "$Replace`$1" } |
    Out-File -FilePath $NewFile
lit
  • 14,456
  • 10
  • 65
  • 119
0

This simpler Batch file do what you want. The trick used to replace just the first appearance of the search string consists in use the !line:*%search%=!" replacement, that replaces from the beginning of the line up to the first appearance of the search string, twice: first to get the "tail" of the line, and then to get the "head" of the line by removing the tail.

@echo off
setlocal EnableDelayedExpansion

set "base=fox"
set "search=brown"
set "change=white"

rem Assemble a list of line numbers that contains *both* terms
set "list="
for /F "delims=:" %%a in ('findstr /N "%base%" test.txt ^| findstr "%search%"') do (
   set "list=!list! %%a"
)

if not defined list echo Search lines not found & goto :EOF

rem Get the first line number
for /F "tokens=1*" %%x in ("%list%") do set "lineNo=%%x" & set "list=%%y 0"

rem Process the file
for /F "tokens=1* delims=:" %%a in ('findstr /N "^" test.txt') do (
   set "line=%%b"
   if %%a equ !lineNo! (
      rem Line with both search strings found:

      rem Replace the term in line, just the first time
      set "tail=!line:*%search%=!"
      for %%x in ("!line:*%search%=%search%!") do set "line=!line:%%~x=%change%!!tail!"

      rem Get next line number from list
      for /F "tokens=1*" %%x in ("%list%") do set "lineNo=%%x" & set "list=%%y
   )
   echo(!line!
)

I changed the line 4 of your input data file to this one in order to get a line with two "brown" strings:

The quick brown fox jumped over the lazy brown dog

This is the output:

The quick white fox jumped over the lazy white dog
The quick red fox jumped over the lazy dog
The quick green wolf jumped over the lazy brown dog
The quick white fox jumped over the lazy brown dog
The quick red lion jumped over the lazy dog
The quick green pig jumped over the lazy brown dog
The quick brown fox jumped over the lazy dog

You could also solve this problem in a very different way using my printf.exe version 2.11 program.

My printf.exe application is a Windows console program that is a wrapper for the well-known printf CRT function, thus allowing text and formatted numeric values ​​to be displayed in the cmd.exe window. In addition, my program printf.exe also allows to perform Reverse Polish Notation arithmetic operations on 32-bit integers and 64-bit double floating-point numbers using the same method and functions of the stack-based Hewlett-Packard calculators.

The new printf.exe version 2.11 also manages character string operations and allows to write scripts (programs) using the simplest programming scheme. The Batch file below contains a printf.exe program that also solves this problem. Note that the printf.exe's method (clearly explained in the extensive comments) is even simpler than the previous Batch file's one.

@echo off
setlocal

rem Replace a string if another string is present in the line using Aacini's printf.exe
rem Antonio Perez Ayala

set "base=fox"
set "search=brown"
set "change=white"

< test.txt printf "%%s\n"        /* line format */ ^
   (            /* WHILE NOT EOF                */ ^
      /" <* /"  /*    Clear the stack           */ ^
      80        /*    80   (max line length)    */ ^
      ( IN? )?  /*    "The quick brown fox jumped over the lazy brown dog" len      */ ^
   ;            /* EOF? QUIT                    */ ^
      /" < /"   /*    "The quick brown fox jumped over the lazy brown dog"          Drop len                 */ ^
      ]0        /*    "The quick brown fox jumped over the lazy brown dog"          Store line address in R0 */ ^
      base      /*    "The quick brown fox jumped over the lazy brown dog" "fox"    Load base search string  */ ^
      index     /*    "The quick brown fox jumped over the lazy brown dog" 16 1     Indices of "fox"         */ ^
      ==0?      /*    IF line have not "fox":   */ ^
        OUT     /*       Show it                */ ^
   :            /*       and CONTINUE the loop  */ ^
      /" <* /"  /*    Clear stack               */ ^
      [0        /*    Recall string address     */ ^
      1 dups    /*    "The quick brown fox jumped over the lazy brown dog"          Recall string      */ ^
      search    /*    "The quick brown fox jumped over the lazy brown dog" "brown"  Load search string */ ^
      index     /*    "The quick brown fox jumped over the lazy brown dog" 10 41 2  Indices of "brown" */ ^
      ==0?      /*    IF line have not "brown": */ ^
        OUT     /*       Show it                */ ^
   :            /*       and CONTINUE the loop  */ ^
      shift     /*    "The quick brown fox jumped..." 41 1 10                   Get first index      */ ^
      [0        /*    "The quick brown fox jumped..." 41 1 10 R0                String address       */ ^
      +         /*    "The quick brown fox jumped..." 41 1 R0+10                Address of "brown"   */ ^
      0 movc1   /*    "The quick 0rown fox jumped..." 41 1 R0+10                Cut the string there */ ^
      search    /*    "The quick 0rown fox jumped..." 41 1 R0+10 "brown"        Load search string   */ ^
      len       /*    "The quick 0rown fox jumped..." 41 1 R0+10 "brown" 5      Len of "brown"       */ ^
      /" <2 /"  /*    "The quick 0rown fox jumped..." 41 1 R0+10 5              Drop "brown"         */ ^
      +         /*    "The quick 0rown|fox jumped..." 41 1 R0+10+5              Address of end of "brown" */ ^
      ]1        /*    "The quick 0rown|fox jumped..." 41 1 R0+10+5              Store it in R1       */ ^
      FMT{ "%%s" [0 OUT        /*    Show first part: "The quick "      */ ^
           /" < /" change OUT  /*    Show replacement: "white"          */ ^
      FMT}                     /*    Format end                         */ ^
      /" <* /" [1 OUT          /*    Show second part: " fox jumped..." */ ^
   :            /* REPEAT                       */ ^
   )            /* ENDWHILE                     */

Although it may seem complicated, the printf.exe instructions are very simple. HP calculator users can start writing printf.exe programs in minutes after understanding the differences and the programming scheme. You can review another example of a printf.exe program at this answer

You can download the printf.exe package from this link

Aacini
  • 65,180
  • 12
  • 72
  • 108