Determine Number of Tokens - BATCH - for-loop

I'm currently working on a mass user creation script through PowerShell and Batch. At the moment the script is 95 lines and is the largest script I've ever written in Batch.
I want the script to be as automated as possible and plan to give it to clients that need help creating a mass number of users. To do this, I have a 21 line settings file and one of the variables that I need is the full domain name (This is needed for dsadd)
The problem with this is that users may have any number of variables for this - anywhere from two in testlabs to four in places like schools. I am so far able to separate the tokens however I need them all stored as variables like %%a, %%b and not to store everything as %%a. The number of tokens will be dynamic so I need some sort of solution to this. Something like this (Yes I know this is not the correct syntax):
if number of tokens=4 (
dsadd "cn=%%a,ou=%%b,dc=%DSuffix1%,dc=%DSuffix2%,dc=%DSuffix3%,dc=%Dsuffix4% )
In that line %%a and %%b are variables in another for loop later in the code that reads from a user list excel file. I would need something like that for anything from two tokens to four tokens. I don't mind if the solution to this is not purely BATCH however that is my preferred option.
Thanks.
EDIT: Here is the for loop I have at the moment, this is nested in another larger for loop that adds the users:
for /f "tokens=1,2,3,4 delims=." %%a in (Settings.ini) do (
set L=22
if !L!=22 set DSuffix1=%%a&& set DSuffix2=%%b&& set DSuffix3=%%c&& set DSuffix4=%%d
)
The settings.ini file contains various settings such as the Exchange server and path to the user's home directory. The line I need to interpret is line 22 which looks like this:
DOMAINNAME.SUFFIX(.SUFFIX.SUFFIX.SUFFIX)
A real life example would be:
testlab.local
or
testlab.ghamilton.local
For a testlab the setting should only be the domainname and suffix although for others such as schools or institutions the number of domain suffixes can go up to four. I want to interpret this.
EDIT: Managed to indent code correctly, sorry.

If I understand you right, you have a string such as domain.foo.bar.baz and you want to change that to dc=domain,dc=foo,dc=bar,dc=baz.
How about this?
set domain=domain.foo.bar.baz
echo dc=%domain:.=,dc=%
That should echo this:
dc=domain,dc=foo,dc=bar,dc=baz
That %domain:.=,dc=% line is expanding the %domain% environment variable, but replacing all . with ,dc=. See http://ss64.com/nt/syntax-replace.html for more details.
To do string replacement in an environment variable, you do need the value to be in an environment variable first. Here's a script that will read line 22 from settings.ini, combined with the above technique:
setlocal enabledelayedexpansion
set L=0
for /f %%a in (settings.ini) do (
set /a L=!L! + 1
if !L! equ 22 (
set domain=%%a
echo dc=!domain:.=,dc=!
)
)
L is being used to count what line we're on. When we reach line 22, we set an environment variable to the value of the entire line (%%a), then do the string substitution as above (but with ! rather than % to take advantage of delayed expansion).
Of course instead of an echo, you would do something like
dsadd "cn=%cn%,ou=%ou%,dc=!domain:.=,dc=!"

Here's my little snippet to sort of demonstrate how to get the separate tokens (the delims are space and tab, but you can change them).
#echo off & setlocal enabledelayedexpansion
for /f "tokens=1,2,3,4" %%a in (test.txt) do (
set token1=%%a
set token2=%%b
set token3=%%c
set token4=%%d
set full=%%a%%b%%c%%d
)
set token
set full
pause>nul
test.txt contains:
first second third
The output was:
token1=first
token2=second
token3=fourth
full=firstsecondthird
So, you could judge how many tokens there is by something like
for /l %%i in (4,-1,1) do if not defined token%%i set amount=%%i
Or something along the same lines.
Hope that helps.

Related

Batch File - Read specific line, and save a specific string in that line as a variable

Is there any way to get for /f loop (or anything else) to read a specific line?
Here is the code I have so far, it reads first word of every line.
#echo off
set file=readtest.txt
for /f "tokens=1 delims= " %%A in (%file%) do (echo %%A)
pause
If someone can point me in the right direction, it'd be much appreciated.
Thanks
Additional Information: I want to make a batch file which will rename a TXT file to a string within that TXT file, located at a specific location. I have figured out how to rename files, all I need to learn to do is to retrieve a string (located at a specific location) with in the file which will go into the name of that TXT file.
Since you haven't fully defined what you mean by "a specific location", I'll make some (reasonable, in my opinion) assumptions, though the method I present is equally valid no matter what your definition turns out to be.
You can get arbitrary lines and arbitrary words on that line by using a line counter variable in conjunction with tokens.
Let's assume your text file name can be found as the second argument on the fourth line of the infile.txt file. You can get that with something like:
#setlocal enableextensions enabledelayedexpansion
#echo off
set /a "line = 0"
for /f "tokens=2 delims= " %%a in (infile.txt) do (
set /a "line = line + 1"
if !line!==4 set thing=%%a
)
endlocal & set thing=%thing%
echo %thing%
This actually uses a few "tricks" which warrant further explanation:
the line counter to ensure you only grab what you want from a specific line, though you could change the test !line!==4 into anything you need such as a line beginning with #, the fifth line containing the string xyzzy and so on.
the use of setlocal/endlocal to effectively give you a scope from which variables cannot leak. This is good programming practice even for a language often not normally associated with such things :-)
the use of endlocal & set to bypass that scope so that thing is the only thing that does actually leak (as it should).
the use of delayed expansion and !..! variables to ensure they're correct within the for loop. Without this, the %..% will always be expand to the value they were set to when the for loop started.
Those last two bullet points are actually related. %..% variables are expanded when the command is read rather than when it is executed.
For a for loop, the command is the entire thing from the for to the final ). That means, if you use %line% within the loop, that will be evaluated before the loop starts running, which will result in it always being 0 (the variable itself may change but the expansion of it has already happened). However, !line! will be evaluated each time it is encountered within the loop so will have the correct value.
Similarly, while endlocal would normally clear out all variables created after the setlocal, the command:
endlocal & set thing=%thing%
is a single command in the context of expansion. The %thing% is expanded before endlocal is run, meaning it effectively becomes:
endlocal & set thing=whatever_thing_was_set_to_before_endlocal
That's why the use of setlocal and endlocal & set is a very useful way to limit variables "escaping" from a scope. And, yes, you can chain multiple & set stanzas to allow more variables to escape the scope.

Passing variable out of setlocal code

When writing scripts in Windows batch files, sometimes the proper execution of the script requires the use of the setlocal command. My main complaint about using setlocal is that I'm often performing complicated for & if statements in which I set variable values in that section of code. These settings are lost when I issue the command endlocal.
So far I've worked around this by echoing the variable value into a scratch file within the setlocal segment and then read the value back into the variable after the endlocal. However, it seems like there should be a more elegant solution to the problem.
The suggested answer provides a handy way to circumvent the issue if only using one or multiple set statements. However, the linked answer does not provide a solution when the setlocal is in place to allow a for loop to properly expand variable names at time of execution rather than parsing. In my situation, I also have if statement logic tree to perform further checking on that information to set many different possible variable values. The linked solution does not provide a solution to this situation.
This code is supposed to check an install package for its version number. It is called from another script that requires that version number to function correctly. We know the package is named using the form [application][version number].msi. The [version number] can be any of the following:
7
7.5
9
9.5
10
10.1
It is possible that more than one install package exists in the designated directory so it's important to look through them all and select the highest version in the directory.
I inherited and expanded the code to correctly process the 10 & 10.1 versions. If there's a better way to do this without the setlocal (e.g. I've thought of rewriting to use a case statement) I'd love to see a better solution.
However, I'm still interested in learning how to pass variables out of the setlocal / endlocal segment.
setlocal enabledelayedexpansion
for /f %%a in ('dir /b program*.MSI') do (
set FileName=%%a
set tst1=!FileName:~4,3!
if "!tst1!" == "msi" (
rem Only true if it has a 1 digit number (e.g. "9")
set MAIN_VERSION=!FileName:~2,1!
) else (
set tst2=!FileName:~5,3!
if "!tst2!" == "msi" (
rem Only true if it has a 2 digit version number (e.g. "10")
set MAIN_VERSION=!FileName:~2,2!
) else (
... lots more code ...
)
)
)
rem Write results out to a file for temporary storage. This particular
rem form of echo is required to ensure there are no trailing spaces, CR,
rem or LF
echo|set /P ="!MAIN_VERSION!" > %USERPROFILE%\UGV.txt
rem End local variables
endlocal
rem Read the correct version out of our temporary storage file
set /p MAIN_VERSION=<%USERPROFILE%\UGV.txt
How do I pass a variable (such as MAIN_VERSION above) out of a Windows batch script setlocal / endlocal code segment without using a scratch file?
To preserve variables over the setlocal/endlocal scope, there exists different solutions.
It depends of the situation which one you should use.
1) Simple
Works only outside of parenthesis blocks with simple content, but can produce problems with special characters like !^".
setlocal
set localVar=something simple
...
(
endlocal
set "out=%localVar%"
)
set out
2) Medium
Works also in blocks and can handle the most characters, but can fail with !^ and linefeed/carriage return
if 1==1 (
setlocal EnableDelayedExpansion
set "localVar=something medium nasty & "^&"
for /F "delims=" %%V in ("!localVar!") DO (
endlocal
set "out=%%V"
)
)
set out
3) Advanced
Works for all contents in any situation
SO: preserving exclamation marks in variable between setlocals batch

Batch For Loop Treating Space-delineated Input as Single Token

I'm working on a batch script that is suppose to prompt the user for a list of projects, and then process each of those projects in turn. My thought was that this could be done with a for loop, but it's not working. For some reason it's treating the entire string entered by the user (CanalyzerIF CanoeIF CometIF) as a single token.
echo Enter the names of the projects, deliniating each with a space:
set /P PROJECT_LIST=
echo.
echo DEBUG: PROJECT_LIST is %PROJECT_LIST%
echo These are the projects you specified:
for /F "tokens=*" %%i in ("%PROJECT_LIST%") do (
echo %%i
)
My script output looks like this...
DEBUG: PROJECT_LIST is CanalyzerIF CanoeIF CometIF
These are the projects you specified:
CanalyzerIF CanoeIF CometIF
...when what I expect/want to see is this:
DEBUG: PROJECT_LIST is CanalyzerIF CanoeIF CometIF
These are the projects you specified:
CanalyzerIF
CanoeIF
CometIF
It doesn't seem to matter if I use percent signs (%) or exclamation marks (!) to wrap PROJECT_LIST. Anyone know how to fix this?
try with (plain FOR can be used for itration):
echo Enter the names of the projects, deliniating each with a space:
set /P "PROJECT_LIST="
echo.
echo DEBUG: PROJECT_LIST is %PROJECT_LIST%
echo These are the projects you specified:
for %%i in (%PROJECT_LIST%) do (
echo %%i
)
In addition to #npocmaka's perfect solution of the problem using for without /F, I want to provide a solution with the /F option, mainly for demonstration purpose.
First let's take a look at for /F without option string, so using default options, which are like "tokens=1 delims= _" (_ stands for a tab here). This means to take the first space- or tab-separated token and assign it to a given variable like %%i (that is, the first project in your list) and to ignore the rest.
To get multiple items, you need to specify exactly which ones you want to extract. For instance, to get tokens 2,3,4 & 6, state "tokens=2-4,6".
The first of these tokens is assigned to the given variable %%i, the other ones to %%j, %%k, %%l (which constitute implicitly defined variables).
The special token * means to pass all remaining tokens to a single variable, or, in other words, to treat them as a single token (for example, "tokens=1,*" passes the first token to %%i and all the rest to %%j).
Understanding all this leads us to the main problem using for /F: you need to know how many tokens are available. But for this application, we don't know that.
The following uses option string "tokens=1,*" to extract the first token and all the rest; there is a kind of while-loop wrapped around (composed by if and goto) that is intended to reprocess all the rest until nothing is available any more (herein I explicitly defined the space to be the only acceptable delimiter):
echo Enter the names of the projects, deliniating each with a space:
set /P PROJECT_LIST=
echo.
echo DEBUG: PROJECT_LIST is %PROJECT_LIST%
echo These are the projects you specified:
set PROJECT_TEMP=%PROJECT_LIST%
:LOOP
if not defined PROJECT_TEMP goto :NEXT
for /F "tokens=1,* delims= " %%i in ("%PROJECT_TEMP%") do (
echo. %%i
set PROJECT_TEMP=%%j
)
goto :LOOP
:NEXT
So the main problem in the original code is the option string "tokens=*", which defines to pass all tokens to the variable %%i.
Type for /? for more details on all this.

Windows Command line script decrementing variables in FOR loop based on a user defined starting variable

I'm trying to write what seemed like an easy script but I can't figure it out.
Basically, a user is asked Question 1: "How many (in this case) video files they want to add together to create 1 big video file?"
The user then gets asked Question 2: "what is the name of the file you want to add together?" Now here is the problem I'm having...
How do I create a for loop that asks that Question 2 the amount of times given in the 1st question and saves each answer as a unique variable (I'm guessing at decrementation of the variable)
after I have all the correct file names from the user then the program will call the video program according to the video program syntax (THAT syntax I don't need help with, I understand that part)
ex. (a "?" means i don't know what to put there)
#echo off
set /p howmany=How many files do you want to add?
for /? %%variable(???) in (%howmany%???) do (set /p inputfilename=what is the name of the first file you want to add? inputfilename=filename set %howmany%-1???=%howmany%????)
so if the user answered 5 to the Question 1, then the for loop should ask Question 2 five times and create 5 unique variables for every time the answer is given. inputfilename1 = movie1.mov inputfilename2 = movie2.mov etc..
I've been trying to figure this out for a few days.. I can't rap my head around it. I've done plenty of for commands before but this has got me stumped. My browser history is full of google searches that seems like something people would ask about any kind of files. If I did find anything remotely close to this question it was always asked for a different programming language. My brain is fried. Is this even something possible? Please help and thanks in advance.
Although Martin's answer describe how to create the unique variables, he didn't explained how to read them. When you are talking about "saves each answer as a unique variable" the involved concept here is ARRAY. You need to use Delayed Expansion in order to get the values of the unique variables ("array elements"); for further details, type set /? and look for "delayed expansion". You may read a detailed description about array management in Batch files at this post: Arrays, linked lists and other data structures in cmd.exe (batch) script
#echo off
setlocal EnableDelayedExpansion
set /p howmany=How many files do you want to add?
for /L %%i in (1,1,%howmany%) do (
set /p inputfilename[%%i]=what is the name of the file you want to add?
)
rem Process array elements (just show them in this case)
for /L %%i in (1,1,%howmany%) do (
echo %%i- !inputfilename[%%i]!
)
The example below may help you in understanding array management in an easier way:
#echo off
setlocal EnableDelayedExpansion
rem Create an array of ordinal terms
set i=0
for %%a in (first second third fourth fifth sixth) do (
set /A i+=1
set term[!i!]=%%a
)
rem Previous FOR is equivalent to: set term[1]=first, set term[2]=second, ...
set /p howmany=How many files do you want to add?
for /L %%i in (1,1,%howmany%) do (
set /p inputfilename[%%i]=what is the name of the !term[%%i]! file you want to add?
)
rem Process array elements (just show them in this case)
for /L %%i in (1,1,%howmany%) do (
echo The !term[%%i]! file is !inputfilename[%%i]!
)
Anyway to answer your actual question:
#echo off
set /p howmany=How many files do you want to add?
for /L %%i in (1, 1, %howmany%) do (
set /p inputfilename%%i=what is the name of the first file you want to add?
)
rem Output the variables to check
set inputfilename
Output:
How many files do you want to add? 3
what is the name of the first file you want to add? first
what is the name of the first file you want to add? second
what is the name of the first file you want to add? third
inputfilename1=first
inputfilename2=second
inputfilename3=third
What do you need those N variables for? I would guess you need to pass a list of filenames to some script/command-line application.
So, wouldn't you better do with one variable with (space?-)delimited list of filenames?
Like:
#echo off
set /p howmany=How many files do you want to add?
set list=
:NEXT
if %howmany% leq 0 goto END
set /p inputfilename=what is the name of the first file you want to add?
set list=%list% "%inputfilename%"
set /a howmany=%howmany% - 1
goto NEXT
:END
echo %list%

Is it possible to use TWO spaces as a single Delimiter in CMD?

I don't think this is possible, but I'd like to be able to do this, or possibly use an alternative method...
I have a batch file;
for /f "usebackq tokens=*" %%a in (`wmic process get description, commandline`) do (
*Some Code*
)
I need to be able to take the two answers from each line, and use them individually (basically, use the description to check if a process is running, then after I've killed the process and done some file clean-up work, reload the original process including any command line parameters.
One example of the output for a process I may need to end/re-open might be;
"C:\some folder\some other folder\some_application" -cmd_parameter process_name.exe
Note that the descrption is clearly defined by multiple spaces..
So is there a way of saying
for /f "tokens=* delims= " <--(The delims is TWO spaces, not space OR space)
Another way that may be better could be to replcae all instances of multiple spaces with a special character (i.e. one that is never used in a proces or path), and then use that as my delimeter... Though I don't know if that is even possible..
I'm also open to any alternative methods, as long as I can get the process name (to check against a pre-defined list of processes, and the full path to the exe, plus any command line paramteres given.
Thanks all
In direct answer to you question: No, you cannot specify 2 spaces as a delimiter. You can use SET search and replace to change 2 spaces into some unique character, but determining a unique character that will never appear in your description or command line is easier said then done.
A better alternative is to change the output format of WMIC to LIST - one value per line in the form of propertyName=Value. Each propery value can be stored in a variable, and then when the last property for a process is recorded you can take action using the variable values. WMIC output uses Unicode, and that results in a CarriageReturn character being appended to the end of each variable assignment. The CarriageReturn must be stripped to get the correct results.
#echo off
setlocal
for /f "tokens=1* delims==" %%A in ('"wmic process get description, commandline /format:list"') do (
if "%%A"=="CommandLine" (
set "cmd=%%B"
) else if "%%A"=="Description" (
set "desc=%%B"
setlocal enableDelayedExpansion
set "desc=!desc:~0,-1!"
set "cmd=!cmd:~0,-1!"
echo(
echo Do whatever you need to do with the description and command line.
echo description=!desc!
echo command line=!cmd!
endlocal
)
)
There are a few things you need to be careful of.
1) You could have multiple processes for the same image name. If you kill a process via the image name (description), then you will delete all of them. If you also restart it it based on the command line, then it will be killed again when the next process with the same name is killed. It is probably better to kill the process via the process ID.
2) If you know the image name (description) of the process, then you can restrict your output using the WMIC WHERE clause.
3) The command line reported by WMIC is not always reliable. The process is able to modify the value that is reported as the command line.
Here is a solution that retrieves the process ID and command line for a specific description.
EDIT - I fixed the code below
#echo off
setlocal
for /f "tokens=1* delims==" %%A in ('"wmic process where description='MyApp.exe' get processId, commandline /format:list"') do (
if "%%A"=="CommandLine" (
set "cmd=%%B"
) else if "%%A"=="ProcessId" (
set "id=%%B"
setlocal enableDelayedExpansion
set "id=!id:~0,-1!"
set "cmd=!cmd:~0,-1!"
echo(
echo Do whatever you need to do with the process id and command line.
echo process Id=!id!
echo command line=!cmd!
endlocal
)
)
Note - the WMIC WHERE clause uses SQL syntax. It can be made complex using AND and OR conditions, and it supports the LIKE operator using % and _ as wildcards. I believe the entire expression needs to be enclosed in double quotes when it becomes complex.
Foreword
I'd just like to add this for future readers, because I had this problem, solved it myself and I think it'll be useful just to show how to do this simply. Firstly, dbenham is absolutely correct in his answer that "No, you cannot specify 2 spaces as a delimiter.". Since you can't do it directly using the batch for loop, you can simply make your own that does the job. Again dbenham is correct in saying
"You can use SET search and replace to change 2 spaces into some unique character"
And thats somewhat similar to what I did (with some differences) but for completeness sake I think its good to have it on record. The thing is, simply setting all occurences of double spaces to some other character doesn't always solve the problem. Sometimes we have more than two spaces and what we really want is to delimit strings by more than one space. The problem I was trying to solve here is more like this (from the OP) Ricky Payne
"Another way that may be better could be to replcae all instances of multiple spaces with a special character (i.e. one that is never used
in a proces or path), and then use that as my delimeter... Though I
don't know if that is even possible.."
The answer to that is that It IS possible, and not hard at all. All you need is to be able to
A. loop over each character of the string
B. differentiate single from double (or more) spaces
C. turn a flag on when you encounter a double space
D. turn the double (or more) spaces into a special character or sequence of characters that you can delimit by.
the code
To do Exactly this, I coded this for my own use (edited for clarity):
FOR /F "tokens=* delims=*" %%G IN ('<command with one line output>') DO (SET
"LineString=%%G")
SET /A "tempindex=0"
:LineStringFOR
SET "currchar=!LineString:~%tempindex%,1!"
IF "!currchar!"=="" (goto :LineStringFOREND)
SET /A "tempindex=!tempindex!+1"
SET /A "BeforeSpacePosition=!tempindex!"
SET /A "AfterSpacePosition=!tempindex!+1"
IF NOT "!LineString:~%BeforeSpacePosition%,2!"==" " (goto :LineStringFOR)
:LineStringSUBFOR
IF "!LineString:~%BeforeSpacePosition%,2!"==" " (
SET LineString=!LineString:~0,%BeforeSpacePosition%!!LineString:~%AfterSpacePosition%!
GOTO :LineStringSUBFOR
) ELSE (
SET LineString=!LineString:~0,%BeforeSpacePosition%!;!LineString:~%AfterSpacePosition%!
GOTO :LineStringSUBFOREND
)
:LineStringSUBFOREND
GOTO :LineStringFOR
:LineStringFOREND
ECHO Final Result is "!LineString!"
So if your input (output of the command in the FOR or you can change that FOR loop to take in a string) was:
"a b c a b c"
The output should be in this format:
"a;b;c;a b c"
I have tested this on my own code. However, for my answer here I removed all of my comments and changed some variable names for clarity. If this code doesn't work after putting in your commands feel free to let me know and I'll update it but it SHOULD be working. Formatting on here might prevent a direct copy paste.
Just to show whats actually going on
The program flow is basically like this:
FOR each character
:TOP
grab the next character
set a variable to the current index
set another variable to the next index
IF this or the next character are not spaces, goto the TOP
:Check for 2 spaces again
IF this and the next character are both spaces then
get the string up to (but not including) the current index AS A
get the string after the current index AS B
set the string to A+B
goto Check for 2 spaces again
ELSE we have turned the double or more space into one space
get the string up to (but not including) the current index AS A
get the string after the current index AS B
set the string to A + <char sequence of choice for delimiting> + B
goto TOP to grab the next character
After all characters are looped over
RETURN the string here (or echo it out like I did)
Extra
dbenham says in his answer on this type of method that:
"You can use SET search and replace to change 2 spaces into some
unique character, but determining a unique character that will never
appear in your description or command line is easier said then done."
While this may have been true in the past, my yeilded that (at least for my method correct me if I'm wrong for other cases) you can in fact use a delimiter that definitely WON'T appear in your input. this is accomplished by using multicharacter delimiters. This doesn't allow you to use the standard FOR loop, however you can quite easily do this manually. This is described much more in depth here:
"delims=#+#" - more then 1 character as delimiter
Great thread!
This got me thinking and I came up with a slightly sideways solution that may work well for someone as it did for me.
As the original questions was for the WMIC command, and the output can be CSV format, why not just circumvent the space handling by using the /format:csv switch and setting a comma as the delimiter, and incorporating 'usebackq'?
Of course, this might not work if the data itself from WMIC has commas but waqs perfect in my instance where I wanted only the BootOptionOnWatchDog status
would look something like this:
FOR /F "usebackq skip=1 tokens=1-31 delims=," %a IN (`%windir%\system32\wbem\wmic computersystem list /format:csv`) DO echo %f
which returns:
BootOptionOnWatchDog
Normal boot
I ended using 'skip=2' which would return "Normal Boot"
btw, dont post here often hence posting as a guest, but thought it prudent to put this here as it was this post that helped me come to the answer above.
cheers
-steve (NZ)

Resources