I've tried so sort this for the better part of the morning.
It is actually the same question as this one from 2013, to which no one replied:
batch script with FOR does not work
I'll do my best to format the code so that it is easy to follow and maybe I'll get an answer...
I am doing an archive project from our help desk ticket system.
For sorting purposes, the files will have the ticket number and the date.
However, the ticket number varies in length. To fix this, all ticket numbers are to be 6 digits, with the shorter numbers padded with preceding zeroes (i.e. 1234 becomes 001234).
I can get the ticket number, but I need to find its length to know how many zeroes to add.
This works:
echo off
set my_str=12345
set length=0
:Loop
if defined my_str (
set my_str=%my_str:~1%
set /A "length+=1"
goto Loop)
echo the string is %length% characters long
However, I get a bunch of ticket numbers in a list.
So, I read through this:
set statements don't appear to work in my batch file
And got lost reading this:
http://www.computing.net/howtos/show/batch-script-variable-expansion-win200-and-up/248.html
And I tried this:
setlocal enabledelayedexpansion
echo off
for /f %%a IN (test.txt) do (
set my_str=%%a
set length=0
:Loop
if defined my_str (
set my_str=!my_str:~1!
set /A "length+=1"
echo %length%
goto Loop)
echo the string is %length% characters long
)
But it only reads the FIRST line of test.txt
Why does the FOR /F loop fail?
How do I get it to work?
You do not need to know the length of the ticket number string (when you can assure it won't be longer than 6 characters), you can prefix 5 zeros and split off the last 6 characters then:
set my_str=12345
set my_str=00000%my_str%
set my_str=%my_str:~-6%
echo %my_str%
If you still want to get the string length, consult this post.
You cannot use goto within the for body as goto breaks the for loop context. However, you can call a sub-routine within for, which in turn may contain goto.
Your batch script does exactly what you are describing: reading the first line of a number of files (in your case, only the one file) and determining the length of the first line. I.e. you got the inner part of your for loop wrong.
I don't know the correct solution either but I think that this page may be of some help to you:
http://ss64.com/nt/for_f.html
I hope that helps.
This seems like a XY problem. Instead of worrying about calculating the length of a string, just use the built-in substring methods to create a padded string.
#echo off
setlocal enabledelayedexpansion
for /f %%a IN (test.txt) do (
set my_str=000000%%a
#echo !my_str:~-6!
)
The end result echo'ed will be a six-digit number left-padded with zeroes.
Related
Bit of a weird one. I've been messing around with windows batch and time comparisons to schedule a task. Now I know there's a million better ways to do it, but our work machines are pretty locked down, so a batch script seems like the easiest way to actually achieve what I needed without some crazy workaround. I'm also well aware that the idea behind this is pretty gross. I'm not looking for perfection here, I'm just looking for it to work.
Basically at 17:00 on the 23rd November, I needed to move a file from one place to another. Problem is that I wouldn't actually be physically present at that time due to some other commitments, so I figured a batch script that was stuck in a loop of nothing until a certain date/time would work. So I cobbled together the following quickly the night before:
:dateloop
if %date% LSS 23/11/2019 goto dateloop
:timeloop
if %time% LSS 17:00 goto timeloop
<xcopy operation goes here>
And ran it overnight. So imagine my surprise when I came to my machine the next morning to check my emails and noticed that the file had been moved. Thankfully I was able to revert the file and rescheduled my day to do it manually.
I ran some tests last night, making sure that the script didn't exit upon completion so I could read the echo output. And it turns out that it was all running fine until 2:00, when the script decided "yes, this is after 17:00".
I could test it again tonight, and I do plan to, with the time format written out the long way (17:00:00:00) but I was wondering if anyone could confirm before I do, as that's a good while away, if this is the cause of my problem?
If you type IF /? at the command prompt, the help text will include this:
If Command Extensions are enabled IF changes as follows:
IF [/I] string1 compare-op string2 command
IF CMDEXTVERSION number command
IF DEFINED variable command
where compare-op may be one of:
EQU - equal
NEQ - not equal
LSS - less than
LEQ - less than or equal
GTR - greater than
GEQ - greater than or equal
and the /I switch, if specified, says to do case insensitive string
compares. The /I switch can also be used on the string1==string2 form
of IF. These comparisons are generic, in that if both string1 and
string2 are both comprised of all numeric digits, then the strings are
converted to numbers and a numeric comparison is performed.
As you can see, comparison is on strings unless the strings to compare contain only numeric digits. As time contains the non-numeric characters ":" and ".", the time is treated as a string, and of course "2" comes after the "1" from "17:00".
However, with the command echo [%time%] you'll notice that if the time is less than 10, it adds a space at the front. Therefore, you can get correct results with a string comparison as long as you take the space into account. So replace your problematic statement with the following one:
if "%time%" LSS "17:00" goto timeloop
That should fix it.
If compares strings alphabetically or numbers.
You need to rework your code and compare times as numbers. So you also need a reliable way to get date parts as numbers - date and time variables are not suitable because they can be in different formats depending on your settings.
Try this instead:
#echo off
::GOTO comment macro
set "[:=goto :]%%"
::brackets comment macros
set "[=rem/||(" & set "]=)"
for /f %%# in ('wMIC Path Win32_LocalTime Get /Format:value') do #for /f %%# in ("%%#") do #set %%#
%[:%
echo %day%
echo %DayOfWeek%
echo %hour%
echo %minute%
echo %month%
echo %quarter%
echo %second%
echo %weekinmonth%
echo %year%
%:]%
:dateloop
if %year%%month%%day% LSS 20191132 goto dateloop
:timeloop
if %hour%%minute% LSS 1700 goto :timeloop
I need to create a script, that will remove first six characters from a huge amount of files (with different names). I tried this example from another question, but I want to understand it better:
#echo off
setlocal enabledelayedexpansion
set X=3
for %%f in (*) do if %%f neq %~nx0 (
set "filename=%%~nf"
set "filename=!filename:~%X%,-%X%!"
ren "%%f" "!filename!%%~xf"
)
popd
I can see, that modifying the X in -%X%! I actually trim the X number of first characters from all the files in the folder. I don't know what the ~%X%, is - I can only see that if it is not a number higher than 0, the script won't run. I also don't know what set X=3 is - I can only see that there is no difference whether it is present in the bat file or not. Could anyone please explain to me the syntax of this file?
Thanks in advance!
That method is called Substring.
You can see a lot of examples and explanation here: http://ss64.com/nt/syntax-substring.html
The first number is the start index, and the second number is the last index of.
Example:
#echo off
Set "Filename=TestFile.txt"
Set "Filename=%Filename:~0,-4%"
Echo %FILENAME%
pause
In that code we start reading from index "0" (the first letter of the string), and stop reading at "-4", then we substract from 0 to -4 so the result is: "TestFile"
I hope this helps.
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.
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)
I found a bug in a bat file I was writing and by accident found a fix however I don't understand why the fix worked.. Which is ultimately my question, or if there are other problems with this logic that I have not discovered.
The script does a line count in 2 or more files then the goal is to find the file with the lowest line count and later use that count in a variable later in the script as a parameter.
Example file 1 (9 lines):
Wrote,0
Wrote,1
Wrote,2
Wrote,3
Wrote,4
Wrote,5
Wrote,6
Wrote,7
Wrote,8
Example file 2 (10 lines):
Wrote,0
Wrote,1
Wrote,2
Wrote,3
Wrote,4
Wrote,5
Wrote,6
Wrote,7
Wrote,8
Wrote,9
The main difference that I see is that lines 9 & 10 in file 2 are two digit line numbers
NOTE: I am not referring to the actual value on each line but only the line number's count value as my code does not try to parse the lines but only get the line count.
Near the top of the code I create then append the count values for each file into a temp file called ~numbers.txt so to simulate that process here assume my starting code is:
echo 9 > ~numbers.txt
echo 10 >> ~numbers.txt
type ~numbers.txt
So now I have a file with two lines "9" and "10" and of course 9 is lower than 10 but it also is one digit less than 10.
Now I attempt to find the lowest number contained in the file ~numbers.txt by reading the 1st line into a variable then looping and evaluating if current value is less than prior value.
setlocal enabledelayedexpansion
set /p lowest=<~numbers.txt
FOR /F "delims=" %%G in (~numbers.txt) DO IF %%G LSS !lowest! SET lowest=%%G
echo lowest num = %lowest%
When I run this code I get this output which is wrong:
lowest num = 10
What I have found is that if I remove the "delims=" from the FOR loop I will get the correct result of 9.
ALSO and what really screws up my understanding of the problem; If I only change the values in ~numbers.txt to 10 and 100 and leave the FOR loop line alone (keeping "delims=" ) then I get the correct value of 10 as my answer???
This is bad because it implies that my logic is flawed and that other number combinations will also yield different results.
Can anyone explain to me why this is the case and/or give an example on how to get the correct lowest number regardless of the values?
Thanks...
If you don't see something it doesn't mean it doesn't exist. (I'm talking of spaces here)
Try this instead:
> ~numbers.txt echo 9<press ENTER immediately>
>> ~numbers.txt echo 10<press ENTER immediately>
This will be truly WYSIWYG, no spaces at the end of line, no text comparisons by if because strings are not all numbers.
Of course, stripping the spaces with default delims (one of which is a space) also works fine as you have noticed already.
Consider this way of determining lowest line count:
set /a minlines=0x7FFFFFFF
for /f "tokens=2 delims=:" %%a in ('find /c /v "" *.txt') do set /a minlines="%%a-(minlines-%%a)*(minlines-%%a>>255)"
echo %minlines%
Note: no ifs, no delayed expansion