6

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
aschipfl
  • 33,626
  • 12
  • 54
  • 99
James K
  • 4,005
  • 4
  • 20
  • 28
  • 1
    How can you ask a question and answer it at the same time? [so] is usually not a FAQ site. People ask questions here if they cannot find an answer on their own. – Frank Bollack Jul 27 '12 at 09:55
  • Why are you answering in the 3rd person? :) – Bali C Jul 27 '12 at 09:57
  • 8
    [It’s OK to Ask and Answer Your Own Questions](http://blog.stackoverflow.com/2011/07/its-ok-to-ask-and-answer-your-own-questions/) – Dialecticus Jul 27 '12 at 10:05
  • Agreed - it is definitely OK. I've done it a few times myself. But unfortunately it rubs some people the wrong way, so you will want to be careful. Just make sure the question and answer is novel and useful. You will want to do extra research to make sure it hasn't already been asked and isn't common knowledge. This particular question is a good choice as replacing asterisk is a long standing thorn for those of us that like to work with batch. – dbenham Jul 27 '12 at 12:07
  • 3
    @Frank Bollack, it's an option right on the page when you ask a question. Down at the bottom, a check-box says "**Answer your own question**". If you check **Dialecticus**'s link, you will find the FAQ says **"it is not merely OK to ask and answer your own question, it is explicitly encouraged"** In big, bold letters. And lastly, I searched for the better part of a day, and couldn't find anything that addressed this, so when I figured it out on my own I thought there should be some info about it on the net. – James K Jul 27 '12 at 22:12
  • @Bali C - Good Question. Because that's the format that it specified in the FAQ. Check **Dialecticus**'s link. – James K Jul 27 '12 at 22:18
  • Thanks for this - I spent ages thrashing about for a solution. http://stackoverflow.com/questions/17071465/escape-asterisk-in-windows-batch-files-for-loop/17087350#17087350 – JohnLBevan Jun 13 '13 at 12:42

5 Answers5

12

The easy answer is no.

The problem that you're encountering stems from the fact that the asterisk * is a special character when used with the SET search and replace method. It matches multiple characters in a limited, but still useful, way. You can learn about that here.

The hard answer is Yes!

I will provide you with two solutions. One an incomplete solution but elegent, the other complete and inelegent.

Both methods will search for * and replace it with an x.
Both methods will both search and modify the following string:

*love*

The first method that comes to mind is using a 'FOR /L' statement, and requires that you know how many characters long the environmental variable is.

::Major Edit::

I thought I knew the various maximum size strings of environmental variables, but dbenham has taken me to school, shown me a kick-in-the-behind length function, and in the mean time completely reversed my opinions of the two solutions I'm presenting.

Other than for the Windows 95/98/ME limitation of a 256 Character maximum environmental variable size. It seems that all versions of Windows using CMD.EXE have a limitation of 8,192 characters, well below what the documentation suggests.

Both versions require delayed environmental variable expansion, but for two different reasons. One because I'm operating inside a FOR statement. The other because you cannot put a % pair inside another % pair because the command processor matches the second % that it encounters to the first one it encounters, but we need to use a variable inside another variable expression. (You'll see.)

This solution uses the strLen function (in line 3) from DosTips.com that can be found Here. Just slap it into a file called strLen.bat and be amazed at it's speed!

Solution 1: (FOR /L Solution) :: Preferred Solution ::

setlocal ENABLEDELAYEDEXPANSION
set nam=*love*
rem calling strLen
call :strLen nam len
for /l %%x in (0,1,%len%) do if not "!nam:~%%x,1!"=="" if "!nam:~%%x,1!"=="*" (
    set /a plusone=%%x+1
    for /l %%y in (!plusone!, 1, !plusone!) do (
        set nam=!nam:~0,%%x!x!nam:~%%y!
    )
)
echo %nam%
ENDLOCAL

I think this is a quick and elegant solution It could be sped up by adding the contents of strLen.bat to the routine, but I wanted no confusion as to the author.

If you, for some reason, do not wish to use strLen, then the next quickest method would probably use a GOTO loop.

Solution 2: (Goto Solution)

setlocal ENABLEDELAYEDEXPANSION
set nam=*love*
set num=0

:loop
    set /a plusone=%num%+1
    if "!nam:~%num%,1!"=="*" set nam=!nam:~0,%num%!x!nam:~%plusone%!
    set /a num=%num%+1
if not "!nam:~%num%,1!"=="" goto :loop

echo %nam%
EndLocal

Special thanks to dbenham for pointing out the strLen function. It works faster than any batch based function has a right to!

im_chc
  • 1,023
  • 15
  • 24
James K
  • 4,005
  • 4
  • 20
  • 28
  • You are misinformed - the maximum batch variable length in XP and beyond is 8192 bytes. Windows variables can be larger, but CMD.EXE (batch) is limited to 8192. – dbenham Jul 27 '12 at 11:30
  • Have a look at this very efficient [strLen function](http://www.dostips.com/DtCodeCmdLib.php#strLen) at DOStips - it will dramatically improve your solution. Also look at their [batch function tutorial](http://www.dostips.com/DtTutoFunctions.php) - I think you will like it :-) – dbenham Jul 27 '12 at 11:47
  • @dbenham - I was wrong about Vista, that should be included with Windows 2008/7/8. Here is an MSDN article: http://msdn.microsoft.com/en-us/library/ms682653(VS.85).aspx I will edit my answer to reflect this. – James K Jul 27 '12 at 22:27
  • @dbenham - Thanks for the tips. I *try* to stay within the confines of the tools that Microsoft supplies with it's operating systems, or provide free downloads for. But I still appreciate the tips! :) – James K Jul 27 '12 at 22:30
  • I am well aware of the Windows limits, but you do not realize that CMD.EXE is further constrained: see http://support.microsoft.com/kb/830473. Also, I don't think you understood the links I provided. Everything there is written using pure native batch, so it absolutely meets your goal of MS supplied tools. If you are unwilling to use a batch routine written by someone else, then that would be like me saying I couldn't use any batch script you posted here because MS did not supply it. Trust me, you will be amazed by the performance and usefulness of the strLen routine. – dbenham Jul 27 '12 at 23:51
  • @dbenham - Sorry, I just tested it. 32,767 is a hard number. – James K Jul 27 '12 at 23:54
  • @dbenham - No need to get touchy, I apologize if I seemed condescending or something. I assumed that the strLen function you referred to was an exe, or else I don't understand how it's use would speed up the batch file. But I will go look at it now. – James K Jul 28 '12 at 00:20
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/14550/discussion-between-james-k-and-dbenham) – James K Jul 28 '12 at 00:48
2

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
  • Honestly, I find this more useful than the others — I just want to know *if* there is an asterisk in my string. – jwd Jul 17 '20 at 20:51
1

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
David
  • 134
  • 7
1

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/!_
aschipfl
  • 33,626
  • 12
  • 54
  • 99
-1

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>

Ephellon Grey
  • 392
  • 2
  • 9