Counting files with FOR (command batch) - batch-file

I want count only files in a directory with Windows batch.
(The ultimate purpose is to call a vbs file if I have any files whatsoever.)
Here's what I have so far:
set /a db=0
echo %db%
for /f %%i in ('dir /b') do (
set /a db=%db%+1
echo %db%
)
echo %db%
This will me give the following: 0 0 0 0 1 for the value of %db%
(as I have 3 files in the directory right now)
Maybe trivial, but why won't it increase the value of %db% during the loop, only in the end?
What happens between the last loop of for (where %db% still was 0) and the last line (where %db% is 1 already)?
How should I fix it?
Thanks in advance!

While the batch file is being executed, each line or block of lines (lines enclosed in parenthesis) is first parsed and then executed. During the parse phase, variable read operations (where you retrieve the value of the variable, that is %var%) is removed, being replaced with the value inside the variable. Once this is done, the resulting command is executed.
Inside your for loop you are changing the db variable, but you can not retrieve the changed value. All the read operations were replaced with the value in the variable before the for command start to execute.
The usual way to solve this problem is to enable delayed expansion and change the %var% syntax into !var! syntax where needed. This tells the batch parser that the variable/value substitution must be delayed until the command is executed, not when the line/block is parsed.
setlocal enabledelayedexpansion
set /a db=0
echo %db%
for /f %%i in ('dir /b') do (
set /a db=!db!+1
echo !db!
)
echo %db%
Now the read operations inside the for loop are using delayed expansion.
But, you don't even need it. The set /a uses its own parser that is able to retrieve the value in the referenced variables, so you can use any of those options
set /a db=db+1
set /a db+=1
to change the variable without having to use read syntax.
Also, unless you need to take into consideration hidden files, it is better to not use a for /f processing the output of a dir command that is executed in a separate cmd instance. Just use a for loop
set /a "db=0"
for %%a in (*) do set /a "db+=1"
echo %db%
But, if as you point all you need is to know if you have any file, and not the number of them, all this is not even needed
dir /a-d >nul 2>nul && ( echo there are files ) || ( echo there is not any file )
It just executes the dir command, with folders excluded (/a-d), discarding any output (>nul) or error message (2>nul).
- If any file is found (no errors), the command after the conditional operator && is executed.
- If there is not any file, the dir command fails and the command after the || conditional operator is executed.

If you just want to detect whether or not there are any files, MC ND already showed a reliable way in their answer.
To count the number of files you can let the find command do the work, as it features a /C switch that counts the number of matching lines. If you specify to do an inverse search by /V (so to return all non-matching lines), together with an empty search string "", all lines are returned.
So when the input for find comes from a dir command line that lists all files, the total count of files is returned (2> nul suppresses the error message in case no files are present):
2> nul dir /B /A:-D | find /C /V ""
To capture the count in a variable, use a for /F loop:
for /F %%C in ('
2^> nul dir /B /A:-D ^| find /C /V ""
') do set "COUNT=%%C"
echo %COUNT%
Note that special characters like > and | need to be escaped by preceding with ^ in order for them not to be processed immediately but in the cmd instance initiated by for /F.

Related

how to add new lines to windows hosts with a batch file

i know this was already discussed but i didn't find what i needed.
I need to add new lines at the end of the hosts window file but,
first i need to check if these lines already exist and than adding them.
I tried this:
set "list=examp.com=examp2.com=examp3.com"
SET NEWLINE=^0.0.0.0
for %%a in (%list%) do (
FINDSTR /I %%a %WINDIR%\system32\drivers\etc\hosts)
IF %ERRORLEVEL% NEQ 0 (ECHO %NEWLINE% %%a>>%WINDIR%\System32\drivers\etc\hosts)
pause
but the result in hosts is just 1 line like this:
0.0.0.0 %a
I also want to know if it's possible to change this:
set "list=examp.com=examp2.com=examp3.com"
with another code that will take variables from a txt file.
Your code is not quite as bad as Mofi would suggest. Although it's quite uncommon to use an equal sign as a delimiter for a for loop, it is nevertheless legal syntax. The largest two problems I see are that you're closing your for loop at the end of your findstr statement; and, assuming you fix that, %ERRORLEVEL% would need its expansion delayed. Or you could use the if errorlevel syntax of the if statement (see help if in a cmd console for full details`). Or even better, use conditional execution.
Here's an example using conditional execution. This example also opens your HOSTS file for appending one time, rather than one time for each loop iteration -- a subtle efficiency improvement, true, but a worthwhile habit to practice when writing files with a loop. And because HOSTS by default has attributes set to prevent writing, I stored and removed the read-only / system / hidden / etc. attributes of the hosts file, appended the changes to the file, then restored the attributes back the way they were before.
#echo off & setlocal
set "hosts=%WINDIR%\system32\drivers\etc\hosts"
set "list=examp.com=examp2.com=examp3.com"
SET "NEWLINE=0.0.0.0"
for /f "delims=" %%I in ('attrib "%hosts%"') do set "raw=%%~I"
setlocal enabledelayedexpansion
for /L %%I in (0,1,18) do if not "!raw:~%%I,1!"==" " set "attrs=!attrs!+!raw:~%%I,1! "
endlocal & set "attrs=%attrs%"
attrib -h -s -r -o -i -x -p -u "%hosts%"
>>"%hosts%" (
for %%a in (%list%) do (
>NUL 2>NUL find /I "%%a" "%hosts%" || echo(%NEWLINE% %%a
)
)
attrib %attrs% "%hosts%"

A script that counts and prints every ocurrence of not any file inside a common subfolder in a specific path

Although I'm really a newbie in this field, I want to accomplish a task in batch scripting: There is a determinate folder of company contracts in a determinate path, each of this folders (approx. 400) has a common folder (2016) where there might be a file indicating there has been an inspection in this year. What i want is to print every company folder that has not any file in the common 2016 folder and a count of the times this happens.
This is what i have (and does not work at all):
set c=0
for %i /d in (*) do
for %j in ($%i\2016\*) do
if (%j==NUL) then (#echo $%i c+=1 echo %c)`
If you just want to know if there is a file in the 2016 directory you can do this:
#echo off
SetLocal EnableDelayedExpansion
set count=0
for %%i /d in (*) do (
REM first unset variable
set files=
for %%j in (%%i\2016\*) do (
REM will set variable each time a file is encountered
set files=present
)
if not DEFINED files (
REM No files in directory 2016
echo %%i
set /a count+=1
echo !count!
)
)
EndLocal
exit /b 0
I don't see why you use $ before each %i. If you execute this code from the command line use one % for the loop variables i and j. But in a batch-script you'll have to use two of them (%%i, %%j).
Another thing, c+=1 won't work except if you use set /a.
I used delayed expansion because each block code ( between (...)) is parsed as one single command (as if it was all on one line with && between the commands inside the block) and you can't just assign a new value to a variable and read that new value in the same command. That's also the reason why I use !count! instead of %count% (which will give the value before the block). If you'd rather not use delayed expansion, remove the SetLocal EnableDelayedExpansion and replace echo !count! with call echo %%count%% (is another way to read a new value in the same command)
Also, be aware that each echo will end its output with a carriage retur and a newline. So each echo will result in a new line of output.

Batch file log function for printing command output to both log file and screen

I found there are some topics similar to this problem but not exactly what I want, so I raise this topic.
I want to create a log function for printing message to both formatted log file and console output. The function is as below:
:LOGDEBUG
#echo DEBUG: %~1
if NOT EXIST %LOG_FILE% exit /b 1
#echo [%date - %time%] DEBUG: %~1 >> %LOG_FILE% 2>&1
exit /b 0
And I try to use it for printing the command execution output and if the output contains special character like "<" and ">", this function doesn't work well and prompt "The system cannot find the file specified". My code for executing a command is below:
for /f "usebackq delims=" %%a in (`dir c:\temp`) do (
CALL :LOGDEBUG "%%a"
)
However, when I use "echo" command directly instead of the log function, the output can be printed correctly on the console. Like the following code:
for /f "usebackq delims=" %%a in (`dir c:\temp`) do (
echo %%a
)
May I know what is the problem, and how can I print the output correctly by using the log function? Thanks
You have answered your question by own: when I use "echo" command directly...
#ECHO OFF
SETLOCAL EnableExtensions
set "LOG_FILE=D:\tempx\program.log" my testing value
rem type NUL>"%LOG_FILE%"
for /f "usebackq delims=" %%a in (`dir d:\temp 2^>NUL`) do (
CALL :LOGDEBUG "%%a"
)
rem type "%LOG_FILE%"
ENDLOCAL
exit /b
:LOGDEBUG
FOR %%A in ("%~1") do (
#echo DEBUG: %%~A
if NOT EXIST "%LOG_FILE%" exit /b 1
#echo [%date% - %time%] DEBUG: %%~A >> "%LOG_FILE%" 2>&1
)
exit /b 0
Resources (required reading):
(command reference) An A-Z Index of the Windows CMD command line
(additional particularities) Windows CMD Shell Command Line Syntax
(%~A etc. special page) Command Line arguments (Parameters)
(special page) EnableDelayedExpansion
Here is the batch code which should work.
#echo off
setlocal EnableDelayedExpansion
set "LOG_FILE=C:\program.log"
del "%LOG_FILE%" 2>nul
for /F "delims=" %%a in ('dir "C:\temp\*" 2^>nul') do call :LOGDEBUG "%%a"
endlocal
goto :EOF
:LOGDEBUG
set "StringToOutput=%~1"
echo DEBUG: !StringToOutput!
echo [%DATE% - %TIME%] DEBUG: !StringToOutput!>>"%LOG_FILE%"
goto :EOF
First delayed environment variable expansion is enabled and a copy of existing environment table is made. It is explained below why this is done.
Next the name of the log file with full path is assigned to an environment variable in local variable table. This path can be with or without 1 or more spaces in path. The log file is deleted in case of existing already from a previous run. This code can be removed if you want to append new lines to already existing file. But you should add in this case code to avoid that the log file permanently increases until no free storage space anymore.
The FOR command executes the command DIR and processes each line of the output of DIR written to stdout. Blank lines are skipped. The default delimiters are space, tab and newline characters. As wanted here are the entire lines of DIR, the default delimiter list is replaced by nothing which means only newline characters remain and loop variable %a gets assigned always an entire non blank line.
The output of command DIR contains < and > which are interpreted as redirection operators if found by command processor within a line not quoted. Therefore the line for DIR output is passed quoted to subroutine LOGDEBUG. Which characters must be usually quoted are listed on last help page printed into a command prompt window when executing cmd /? in a command prompt window.
When the loop has finished, the local environment table is deleted which means LOG_FILE and StringToOutput are also removed, and previous environment is restored which usually means the delayed expansion is turned off again before batch execution exits with a jump to predefined label to end of file.
The subroutine LOGDEBUG first assigns the passed string to an environment variable without surrounding quotes just needed because of special characters in line like < and >.
Next the line is written to console window without quotes using delayed expansion as otherwise < and > would be interpreted as redirecting operators and not literally.
The same line is written also to the log file with the difference of date and time inserted at beginning of line. You missed the percent sign after date in your code. Again delayed expansion is used to get the line with the characters < and > written to file without being interpreted as redirection operators.
Important is also that there is no space before >> as otherwise each line in log file would have a trailing space. 2>&1 is useless here as command echo does not write something to stderr.
The subroutine is exited with a jump to end of file resulting in command FOR processes next line.
For understanding the used commands and how they work, open a command prompt window, execute there the following commands, and read entirely all help pages displayed for each command very carefully.
call /?
del /?
dir /?
for /?
goto /?
set /?
It would be of course possible to do all the output directly in body of command FOR without using a subroutine.
#echo off
setlocal EnableDelayedExpansion
set "LOG_FILE=C:\program.log"
del "%LOG_FILE%" 2>nul
for /F "delims=" %%a in ('dir "C:\temp\*" 2^>nul') do (
echo DEBUG: %%a
echo [!DATE! - !TIME!] DEBUG: %%a>>"%LOG_FILE%"
)
endlocal
Delayed variable expansion is nevertheless required here as otherwise %DATE% and %TIME% would be expanded by command processor like %LOG_FILE% already on parsing entire block defined by ( and ) before command FOR is executed at all which would result in same date and time written for all lines to the log file.

Windows 2003 batch script with pipe

I'm trying to write a batch script that will run every 15 minutes, check number of files in a directory and if that number is bigger then set limit, move all files to another directory. I would easily do this in bash script, but this is my first batch script.
I split this task in several steps:
Find number of files in directory.
I managed to do this with this command:
dir/b/a-d d:\test\test2 | find /v /c "::"
Next thing is to assign output of this command to some variable so I can compare it with my desired limit. This is where problem starts.
ECHO OFF
setlocal enableextensions
FOR /F "tokens=* USEBACKQ" %%a IN (`dir/b/a-d d:\test\test2 ^| find /v /c "::"`)
DO (#SET NUMFIL=%%a)
ECHO %NUMFIL%
endlocal
I'm getting: "| was unexpected at this time". Obviously, pipe is getting in the way. I found that it is special character and as such must be escaped with caret. After doing so, I'm getting: "The syntax of the command was incorrect." This is Windows server 2003.
3.After getting this problem solved, I plan to insert something like this:
IF %%NUMFIL%% > 20
(move "d:\test\test2\ti*" "d:\test\test2\dir\")
That would move all that files (all of them starts with "ti") to desired directory.
So my questions would be: what to do with #2 issue and will #3 work in this case?
Not sure ":: will work in your first case, since :: is unlikely to appear in a DIR output. A single colon would suffice, since the /c option of find counts the number of LINES in which the target string occurs.
The secret to the second problem is that the do keyword must occur on the same line as the closing-parenthesis of the IN clause. It is possible to break the structure into
FOR /F "tokens=* USEBACKQ" %%a IN (
' dir/b/a-d d:\test\test2 ^| find /v /c ":" '
) DO (SET NUMFIL=%%a)
Note the # is not required - it suppresses the command's being echoed, which is turned off by the initial #echo off (the # there suppresses the echoing of the ECHO OFF
Also, the parentheses around this set are not required. IF they are used, the open-parenthesis must occur on the same physical line as the do.
You also don't need to use usebackq since you have no need here to change the interpretation of quotes.
Third item - > is a redirector. For a comparison operator, use one of EQU GEQ LSS LEQ GTR NEQ depending on comparison required.
And again, the open-parenthesis must be on the same line as the if. With an else, the close-parenthsis before the ELSE , the ELSE keyword and the open-parenthesis after must all be on the same physical line.
I think this should work. Note that it does not use backticks.
ECHO OFF
setlocal enableextensions
FOR /F %%a IN (' dir /b /a-d "d:\test\test2" ^| find /c /v "" ') DO SET NUMFIL=%%a
ECHO %NUMFIL%
IF %NUMFIL% GTR 20 (move "d:\test\test2\ti*" "d:\test\test2\dir\")

Batch file to read first line of text that hasn't been used, and then mark as used

I have a requirement to, within a windows batch file, read the first available line from a text file, pass it to a variable and mark the name\line as used
An example of the file is below.
apple
pear
orange
The script would start with 'apple', pass 'apple' to a variable to be used later in the script (I know how to do that bit), and then write back that line to read &apple, the '&' works as a marker to say it's been used.
The file would then look like:
&apple
pear
orange
the next time the batch file is run it would take 'pear', pass it to a variable and mark it with a & making it look like:
&apple
&pear
orange
I started by trying to find '&' and then trying to move to the next line, but I'm failing after about 12 hours of trying. This is what I got so far .. not much:
for /f "tokens=1" %l in ('name.txt') do (Find /v "&" /v "^---- ^$") (For /F %n in (%l) do (set NewName=%n))
Thanks
Running this on the.file would modify each line in turn;
#echo off
setlocal enabledelayedexpansion
type nul > the.file.temp
set last=
for /F "tokens=*" %%A in (the.file) do (
set line=%%A
if "!line:~0,1!" neq "&" if "!last!" equ "" (
set last=!line!
set line=^&!line!
)
echo !line! >> the.file.temp
)
echo last value is !last!
type the.file.temp > the.file
(If the line does not begin with & and the variable last is empty, put the line in last & modify line with a leading &. Always append line to a temp file, renaming when done)
Alex k. has a good answer that is probably fine for most situations. (I upvoted.)
However, it will corrupt any text containing !. That limitation can be fixed by toggling delayed expansion on and off within the loop.
The solution is likely to be fast enough for most reasonably sized files. But a FOR loop can become quite slow for large files.
I tested a 190kb file containing 2817 lines, and the Alex K. solution took 20 seconds for one run.
Here is a completely different solution without using any loops that processes the same 190kb file in 0.07 seconds - 285 times faster :)
#echo off
setlocal enableDelayedExpansion
set "file=test.txt"
findstr /bv "$ &" "%file%" >"%file%.available"
set "var="
<"%file%.available" set /p "var="
if defined var (
>"%file%.new" (
findstr /b "&" "%file%"
<nul set /p "=&"
type "%file%.available"
)
move /y "%file%.new" "%file%" >nul
)
del "%file%.available"
echo var=!var!
Update: As requested in comment, here is a heavily commented version of the code.
#echo off
setlocal enableDelayedExpansion
:: Define the file to process
set "file=test.txt"
:: Write the unused lines to a temporary "available" file. We don't want any
:: empty lines, so I strip them out here. There are two regex search strings;
:: the first looks for empty lines, the second for lines starting with &.
:: The /v option means only write lines that don't match either search string.
findstr /bv "$ &" "%file%" >"%file%.available"
:: Read the first available line into a variable
set "var="
<"%file%.available" set /p "var="
:: If var defined, then continue, else we are done
if defined var (
REM Redirect output to a "new" file. It is more efficient to redirect
REM the entire block once than it is to redirect each command individulally
>"%file%.new" (
REM Write the already used lines to the "new" file
findstr /b "&" "%file%"
REM Append the & without a new line
<nul set /p "=&"
REM Append the unused lines from the "available" file. The first appended
REM line is marked as used because of the previously written &
type "%file%.available"
)
REM Replace the original file with the "new" content
move /y "%file%.new" "%file%" >nul
)
:: Delete the temp "available" file
del "%file%.available"
:: Display the result
echo var=!var!
I haven't tested this, but I just realized I could have written the line that writes the available lines to look for lines that start with a character other than &:
findstr "^[^&]" "%file%" >"%file%.available"

Resources