I need to match or replace an asterisk * in a batch environmental variable using only native Windows commands. Is this possible?

Viewed 9299

I'm trying to remove an asterisk from an environmental variable string, but can't seem to do it.

I'm creating an m3u file based around search strings, so for instance I if I want to make an m3u file containing every song with the word love in it, I would enter:

m3u *Love*

And m3u.bat would create the file:

xLovex.m3u

But the regular method of replacing characters does not work with an asterisk. (Though I don't have that problem with the question mark.)

set nam=%nam:*=x%.m3u

Instead creates the filename

x.m3u
5 Answers

Although there were already some very good and robust ways explained here, I'd still like to add another option for the sake of completion.

It's not as good as the other options but I personally use it in some cases where I'd like to keep the code clean and where I know that it will suffice:

The way it works is by using for /f's delims to cut the string into two parts, which are then put back together, getting rid of the * in the process:

for /f "tokens=1,* delims=*" %%a in ("a*b") do (set string=%%a%%b)

>>> string=ab

Obviously, the downside to this is that it can only be used to remove one *.

To remove more, we can either just use more tokens...

for /f "tokens=1-3,* delims=*" %%a in ("a*b*c*d") do (set string=%%a%%b%%c%%d)

>>> string=abcd

... or we can put the first line in a for /l-loop:

setlocal enableDelayedExpansion
set string=a*b*c*d
for /l %%a in (1, 1, 3) do (
    for /f "tokens=1,* delims=*" %%b in ("!string!") do (set string=%%b%%c)
)

>>> string=abcd

Another thing to note is that you can define more than one character in delims, and they will all be removed at once:

for /f "tokens=1,* delims=+-*/" %%a in ("a*-/+b") do (set string=%%a%%b)

>>> string=ab

Another solution to the stated problem is to use a PowerShell replace command within your batch script.

set var=*Love*
echo %var%>var.txt | powershell -command "((get-content var.txt) -replace '[\x2A]','x') -replace '.{1}$' | set-content var.txt"
set /p var=<var.txt
set var=%var%.m3u
echo %var%

In the above code, the second line

  • writes your string into a text file
  • calls a PowerShell command to get the contents of that file
  • replaces the * character with null
  • overwrites the text file with the new value

Once that is done, you read the value back into your variable.

To further explain the replace command, the first single quotes is what you are searching for. We are using square brackets to identify the * character as a hex character (\x2A is the hex value for *). After the comma, the second set of single quotes contains no value so that the searched object is removed. To prevent a space between xLovex and the .m3u, we have to use -replace '.{1}$' before writing the result to the text file.

Once you are done with the text file, enter a line to delete it.

if exist var.txt del var.txt

Here is an approach that does not walk through all characters of a string, but it uses a for /F loop to split the string at every occurrence of a (sequence of a) certain character. The actual functionality is packed into a sub-routine for easy reuse, so the main section of the following script just contains some code to test:

@echo off
setlocal EnableExtensions DisableDelayedExpansion

::This is the main routine of the script holding code for test and demonstration:

rem // Definition of some sample text to test (note that `%%` becomes one literal `%`):
set "DATA=some text,"^&"&;0'@%%~#`$:wild**card*?.re<dir>=|+([{parens}])-^/equal==to=!_"

echo/
call :REPL_CHAR TEXT DATA "*" "?"
setlocal EnableDelayedExpansion
echo(In: !DATA!
echo(Out:!TEXT!
echo/
echo(In: !TEXT!
call :REPL_CHAR TEXT TEXT "=" "/"
echo(Out:!TEXT!
endlocal

endlocal
exit /B


:REPL_CHAR
    ::This function replaces in a string every occurrence of a sequence of a certain character
    ::by another character or a string. It even correctly handles the characters `*` and `=`.
    ::  USAGE:
    ::    call :REPL_CHAR ref_output_string ref_input_string val_search_char val_replace_char
    ::  PARAMETERS:
    ::    ref_output_string   reference to (name of) variable to receive the resulting string;
    ::    ref_input_string    reference to variable that holds the original string; if empty
    ::                        (`""`), the variable referenced by `ref_output_string` is used;
    ::    val_search_char     single character that is to be replaced;
    ::    val_replace_char    character or string to replace every sequence of `val_search_char`
    ::                        with; this may even be empty;
    rem // Localise environment and detect whether delayed expansion is enabled (needed later):
    setlocal & set "$NDX=!"
    setlocal DisableDelayedExpansion
    rem // Fetch arguments and verify them:
    set "#RET=%~1" & if not defined #RET endlocal & endlocal & exit /B 2
    set "#STR=%~2" & if not defined #STR set "#STR=%#RET%"
    set "CHR=%~3"
    if not defined CHR endlocal & endlocal & exit /B 1
    set "RPL=%~4"
    setlocal EnableDelayedExpansion
    rem // Initialise several auxiliary variables:
    set "TST=!%#STR%!" & set "CHR=!CHR:~,1!" & set "INS="
    if "!CHR!"=="_" (set "BUF=#" & set "WRK=!TST!#") else (set "BUF=_" & set "WRK=!TST!_")
:REPL_CHAR_LOOP
    rem // Check whether the end of the string has been reached:
    if not defined TST set "BUF=!BUF:~1,-1!" & goto :REPL_CHAR_NEXT
    rem // Split the string at the next sequence of search characters:
    for /F tokens^=1*^ delims^=^%CHR%^ eol^=^%CHR% %%S in ("!BUF!!INS!!WRK!") do (
        rem // Store the portions before and after the character sequence:
        endlocal & set "BUF=%%S" & set "TST=%%T" & set "WRK=%%T" & setlocal EnableDelayedExpansion
    )
    rem // Loop back and find the next character sequence:
    set "INS=!RPL!" & goto :REPL_CHAR_LOOP
:REPL_CHAR_NEXT
    rem // Return the resulting string with all special characters properly handled:
    if not defined $NDX if defined BUF set "BUF=!BUF:"=""!^"
    if not defined $NDX if defined BUF set "BUF=!BUF:^=^^^^!"
    if not defined $NDX if defined BUF set "BUF=%BUF:!=^^^!%" !
    if not defined $NDX if defined BUF set "BUF=!BUF:""="!^"
    for /F "delims=" %%S in (^""!BUF!"^") do endlocal & endlocal & endlocal & set "%#RET%=%%~S" !
    exit /B

The input and output data of this script (let us call it repl_char_demo.bat) are:

>>> repl_char_demo.bat

In: some text,"&"&;0'@%~#`$:wild**card*?.re<dir>=|+([{parens}])-^/equal==to=!_
Out:some text,"&"&;0'@%~#`$:wild?card??.re<dir>=|+([{parens}])-^/equal==to=!_

In: some text,"&"&;0'@%~#`$:wild?card??.re<dir>=|+([{parens}])-^/equal==to=!_
Out:some text,"&"&;0'@%~#`$:wild?card??.re<dir>/|+([{parens}])-^/equal/to/!_

This is a script that uses for /L loops to walk through all characters of the string, to check each character against a predefined one and replaces it as specified. This method replaces every single matching character rather than sequences. Again the functionality is put into a sub-routine (the main section is dismissed this time):

:REPL_CHAR
    ::This function replaces in a string every occurrence of one certain character by another
    ::character or a string. It even correctly handles the characters `*` and `=`, as well as
    ::sequences of search characters so that every single one becomes replaced.
    ::  USAGE:
    ::    call :REPL_CHAR ref_output_string ref_input_string val_search_char val_replace_char
    ::  PARAMETERS:
    ::    ref_output_string   reference to (name of) variable to receive the resulting string;
    ::    ref_input_string    reference to variable that holds the original string; if empty
    ::                        (`""`), the variable referenced by `ref_output_string` is used;
    ::    val_search_char     single character that is to be replaced;
    ::    val_replace_char    character or string to replace every single `val_search_char`
    ::                        with; this may even be empty;
    rem // Localise environment and detect whether delayed expansion is enabled (needed later):
    setlocal & set "$NDX=!"
    setlocal DisableDelayedExpansion
    rem // Fetch arguments and verify them:
    set "#RET=%~1" & if not defined #RET endlocal & endlocal & exit /B 2
    set "#STR=%~2" & if not defined #STR set "#STR=%#RET%"
    set "CHR=%~3"
    if not defined CHR endlocal & endlocal & exit /B 1
    set "RPL=%~4"
    setlocal EnableDelayedExpansion
    rem // Initialise several auxiliary variables:
    set "WRK=!%#STR%!" & set "CHR=!CHR:~,1!" & set "BUF="
    rem // Loop through all characters and check for match:
    if defined WRK for /L %%J in (0,1,63) do for /L %%I in (0,1,127) do (
        set /A "POS=%%J*64+%%I" & for %%P in (!POS!) do (
            set "TST=!WRK:~%%P,1!" & if not defined TST goto :REPL_CHAR_QUIT
            rem // Store character or replacement depending on whether there is a match:
            if "!TST!"=="!CHR!" (set "BUF=!BUF!!RPL!") else (set "BUF=!BUF!!TST!")
        )
    )
:REPL_CHAR_QUIT
    rem // Return the resulting string with all special characters properly handled:
    if not defined $NDX if defined BUF set "BUF=!BUF:"=""!^"
    if not defined $NDX if defined BUF set "BUF=!BUF:^=^^^^!"
    if not defined $NDX if defined BUF set "BUF=%BUF:!=^^^!%" !
    if not defined $NDX if defined BUF set "BUF=!BUF:""="!^"
    for /F "delims=" %%S in (^""!BUF!"^") do endlocal & endlocal & endlocal & set "%#RET%=%%~S" !
    exit /B

There are actually two nested for /L loops rather than a single one, both of which become broken as soon as the end of the string is reached, using the goto command. Breaking a for /L loop means that it completes iterating in the background although its body is no longer executed. Therefore, using a single loop takes much more time to finish after being broken rather than two nested ones.

The input and output data of this script (with the same main section as above) are:

>>> repl_char_demo.bat

In: some text,"&"&;0'@%~#`$:wild**card*?.re<dir>=|+([{parens}])-^/equal==to=!_
Out:some text,"&"&;0'@%~#`$:wild??card??.re<dir>=|+([{parens}])-^/equal==to=!_

In: some text,"&"&;0'@%~#`$:wild??card??.re<dir>=|+([{parens}])-^/equal==to=!_
Out:some text,"&"&;0'@%~#`$:wild??card??.re<dir>/|+([{parens}])-^/equal//to/!_

See this answer, and with set-ast.bat you'll want to put set-ast nam "x" in your file where needed.

set-ast takes the parameters <variable-to-modify> <string-to-replace-asterisks-with>

Related