I'm trying to create a list of files created yesterday.
I've got the below working without /c:"cmd /c echo #path" but I want the full path.
forfiles /s /d -1 /m *.txt /c:"cmd /c echo #path" > a.txt
forfiles /s /m *.txt /c:"cmd /c echo #path" > b.txt
FOR /f "delims=; tokens=*" %%X IN ('findstr /v /g:a.txt b.txt') DO (
echo %%X
)
What is the best way to get around the issue with using findstr with a path containing backslashes? Do I need to replace all the backslashes in the comparison files with some other symbol and then change it back later or is there an easier way to do it in the findstr line?
The findstr command checks the very first search expression and changes to regular expression mode when a meta-character is found or to literal mode otherwise, unless you explicitly predefine the mode by /L (literal) or /R (regular expression).
But findstr is a nasty beast since there are still some problems even with /L:
wrong results may be returned with multiple literal search strings, unless you specify /I to do case-insensitive searches; but this is not a problem here anyway since you are dealing with directory and file names, which are treated case-insensitively by Windows anyway;
although in literal mode, escaping of meta-characters like ., [, ], ^, $, \, * and ? still occurs when there is a \ in front; you could just double all \ to work around that;
So the following code should work in most situations; delayed expansion is enabled herein by cmd /V, which is required to read the interim variable FILE that is written and read in the same command line:
forfiles /S /M "*.txt" /D -1 /C "cmd /C if #isdir==FALSE (set FILE=#path) & cmd /V /C echo(!FILE:\=\\!" > "exclude.txt"
forfiles /S /M "*.txt" /C "cmd /C if #isdir==FALSE echo #path" > "all.txt"
for /F "delims=" %%X in ('findstr /V /L /I /X /G:"exclude.txt" "all.txt"') do (
echo(%%X
)
I inserted if #isdir==FALSE here to not match directories whose names end in .txt. Also I added /X to findstr in order to match whole lines/paths only.
Regard that literal findstr search strings are limited to a length of 511 bytes (after doubling \), which can easily be reached with file paths.
However, what about a different approach that avoids findstr at all?
Here is a post I once provided for returning items newer than a relative date with forfiles: FORFILES date -after- (date calc in cmd file).
I prefer powershell for such tasks:
Get-ChildItem -Path "C:\Users\tivrick" -Recurse -ErrorAction SilentlyContinue | Where-Object { -Not $_.PSIsContainer -And $_.CreationTime.Date -Eq (Get-Date).Date.AddDays(-1) } | Select-Object -ExpandProperty FullName
If you really need to run that from a batch-file:
#PowerShell -NoP "GCI "C:\Users\tivrick" -R -EA SilentlyContinue|?{!$_.PSIsContainer -And $_.CreationTime.Date -Eq (Get-Date).Date.AddDays(-1)}|Select -Exp FullName"
Related
I am using a cmd batch code to delete all files inside F:\Work folder except a file txt1.txt, the folder contains two files txt1.txt and txt2.txt.
This is the batch code I am using:
forfiles /p "f:\Work" /s /m *.* /C "cmd /c if not #file==txt1.txt del #path"
It is deleting both txt1.txt and txt2.txt. What is wrong?
There is no need to use forfiles for your task, a standard for loop is perfectly suitable:
for /R "F:\Work" %%I in (*) do if /I not "%%~nxI"=="txt1.txt" del "%%~I"
However, if you insist on using forfiles, then:
Do not specify mask /M *.* but use /M * instead (which is anyway the default), because the former skips files with no extension.
Regard that forfiles iterates over both files and directories, so use /C "cmd /C if #isdir==FALSE …" to process files only.
Escape quotes inside of forfiles' /C string, like \" or 0x22. (I prefer the latter since the " in \" is still recognised by the command processor, which might require additional ^-escaping in some situations, potentially leading to quite complicated and illegible escape sequences.)
Note that all file-name-related #-variables return quoted values, so if [not] #file==txt1.txt will never [not] match, but if [not] #file==\"txt1.txt\" will [not], and if /I [not] #file==\"txt1.txt\" will even ignore the case (as said escape " like \").
Hence the correct command line is:
forfiles /S /P "F:\Work" /M * /C "cmd /C if #isdir==FALSE if /I not #file==\"txt1.txt\" del #path"
Or, with alternative quote escaping and skipped mask:
forfiles /S /P "F:\Work" /C "cmd /C if #isdir==FALSE if /I not #file==0x22txt1.txt0x22 del #path"
This is easy enough with PowerShell. PowerShell can run on all supported Windows systems. When you are satisfied that the correct files will be deleted, remove the WhatIf switch.
Remove-Item -Path 'F:\work\*' -Recurse -Exclude txt1.txt,txt2.txt -WhatIf
If you must run from a cmd.exe shell or in a .bat file script,
powershell.exe -NoLogo -NoProfile -Command ^
"Remove-Item -Path 'F:\work\*' -Recurse -Exclude txt1.txt,txt2.txt" -WhatIf
I have been trying to make this work for longer than I care to admit but for some reason I cannot figure it out. I usually work with Linux/Unix.
I simply want to search a directory for all instances where a filename matches a string.
Some things I have tried:
dir /s "/path/to/Test*"
dir /s/b "C:/path/to/Test*"
Additionally, I am hoping to return something that can easily be imported into an array. Something without unnecessary information. All I need Is the path or at the very least the filename for each file matched.
Edit: I dont want information like this (if possible)
Volume in drive C is OS
Volume Serial Number is...
Edit: Test* is intended to indicate all filenames beginning with Test. So TestA, TestB, & TestC should all match.
The same commands work on Linux, Mac, and Windows. http://github.com/PowerShell/PowerShell/
PS C:\src> (Get-ChildItem -Recurse -File -Path 'C:/src/d2' -Filter 'test*').FullName
C:\src\d2\test.bat
C:\src\d2\test.ps1
C:\src\d2\test.sql
C:\src\d2\test.txt
C:\src\d2\copyt\test.txt
Using command aliases, it can be shorter for interactive use. But, aliases are not a good practice for scripts.
PS C:\src> (ls -r -file 'C:/src/d2/test*').FullName
C:\src\d2\test.bat
C:\src\d2\test.ps1
C:\src\d2\test.sql
C:\src\d2\test.txt
C:\src\d2\copyt\test.txt
If you want an array, this will make one.
PS C:\src> $files = (ls -r -file 'C:/src/d2/test*').FullName
PS C:\src> $files.GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Object[] System.Array
Replace directory separators with -replace.
(Get-ChildItem -Recurse -Path 'C:/src/d2' -Filter 'test.*').FullName -replace '\\','/'
Join them to a single line with -join. This join uses a COMMA. Note that if the join uses a COLON in *NIX-style, it will not work well on Windows.
PS C:\src> (Get-ChildItem -Recurse -Path 'C:/src/d2' -Filter 'test.*').FullName -replace '\\','/' -join (',')
C:/src/d2/test.bat,C:/src/d2/test.ps1,C:/src/d2/test.sql,C:/src/d2/test.txt,C:/src/d2/copyt/test.txt
If you need a PATH-style separator, use:
(Get-ChildItem -Recurse -Path 'C:/src/d2' -Filter 'test.*').FullName -replace '\\','/' -join ([IO.Path]::PathSeparator)
I've just been searching for all files, called "test.*" all over my C:-drive, using this simple command:
dir /S C:\test*
Although I just mention the directory C:\, the /S makes sure all subfolders are used too. In top of that, there are no double quotes, as you can see.
Does this solve your issue?
Sorry, I didn't see that you're used working with UNIX/Linux, so here I have an approach you'll prefer:
forfiles /P C:\ /S /M test* /C "cmd /c echo #path"
This does the following:
/P C:\ Start looking in C:\
/S Search through subdirectories
/M test* Filename looks like "test*"
/C <cmd> When found, launch <cmd>
"cmd /c echo #path" Echo (write to output) the complete path of the found file or directory
This will give you a list of files and directories, written as full paths, something like:
"C:\Octave\Octave-5.2.0\mingw64\bin\test-libiberty.exe"
"C:\Octave\Octave-5.2.0\mingw64\bin\test-lua.exe"
"C:\Octave\Octave-5.2.0\mingw64\bin\test-tinyxml.exe"
"C:\Octave\Octave-5.2.0\mingw64\include\llvm\Testing" <-- this is a directory
"C:\Octave\Octave-5.2.0\mingw64\include\wx-3.0\wx\testing.h"
...
Which resembles a lot the typical UNIX/Linux results, you're used to.
More information about forfiles can be found, launching forfiles /?.
Based upon your statement, "I am hoping to return something that can easily be imported into an array", I'd assume you're probably looking for something more like this:
#Echo Off
Rem Ensure that extensions are enabled (required for SET and FOR commands)
SetLocal EnableExtensions
Rem Ensures that there are no existing variables in the environment with names beginning with file[
For /F "Delims==" %%G In ('" Set file[ 2> NUL "') Do Set "%%G="
Rem Gets every file matching the glob test* in the tree rooted at "C:\path\to"
Rem and defines an incremented variable name for each, beginning at %file[1]%
For /F "Tokens=1,* Delims=]" %%G In (
'" Dir /B /S /A-D "C:\path\to\test*" 2> NUL | "%__AppDir__%find.exe" /N /V "" "'
) Do Set "file%%G]=%%H"
Rem An example line to show you all of the variables you have now defined
Set file[ 2> NUL
Rem Pause the script to ensure that you have been able to read any output
Pause
Rem An example line to show you all the first defined variable value
Echo=%file[1]%
Rem Pause the script to ensure that you have been able to read any output
Pause
...and here it is without the Remarks:
#Echo Off
SetLocal EnableExtensions
For /F "Delims==" %%G In ('" Set file[ 2> NUL "') Do Set "%%G="
For /F "Tokens=1,* Delims=]" %%G In (
'" Dir /B /S /A-D "C:\path\to\test*" 2> NUL | "%__AppDir__%find.exe" /N /V "" "'
) Do Set "file%%G]=%%H"
Set file[ 2> NUL
Pause
Echo=%file[1]%
Pause
Don't forget to change C:\path\to\test* as required for your specific location and glob.
forfiles /S /M * /C "cmd /c del if #fsize LEQ 148576 echo #path"
I tried this to delete files which its file size is lower than 148576, but It deleted all of files regardless of the size. Which point should I change to fix it?
The syntax is as follows.
if something matches/don't match/larger/smaller/like something_else do something.
Therefore it should be:
forfiles /S /M * /C "cmd /c if #fsize LEQ 148576 echo #path"
Replace echo with del once you are happy with the printed results.
We match the size with a value, if that matches the operator, we perform an action.
If you wanted to do this with PowerShell, the -WhatIf switch can be invaluable for experimenting without doing damage. When you are confident that the correct files will be deleted, remove the -WhatIf switch.
Get-ChildItem -File -Recurse -Path '.' |
ForEach-Object {if ($_.Length -le 148576) {Remove-Item -Path $_.FullName -WhatIf}}
On windows server 2012, I can see the number of files with the following command but I need to print this file number in []. I have not reached a conclusion in my research on how it is done, can you help?
dir / b / s / a-d | find / v / c "::"
The output of this command is: 213123,
I [33432] (must be in square brackets) I want to write this.
Windows version: Windows Server 2012
How can I resolve it?
Use a FOR /F statement in a .bat script to generate the count and print it out.
FOR /F %%a IN ('DIR /B /S /A:-D ^| FIND /V /C "::"') DO (ECHO [%%a])
If this is done at the command line (not in a .bat script), then do not double the percent character.
FOR /F %a IN ('DIR /B /S /A:-D ^| FIND /V /C "::"') DO (ECHO [%a])
If you wanted to push ahead into PowerShell, you could use:
Get-ChildItem -File | Measure-Object | ForEach-Object { '[' + $_.Count + ']' }
Or, with all the cryptic aliases that should never be written into a script:
gci -File|measure|%{'['+$_.Count+']'}
I have a bunch of *.mp4 & *.jpg files created on an hourly/daily basis, which use the file structure below:
-- before:
c:\video\2017-10-15\21hour\jpg\12.13.15[M][0#0][0].jpg
c:\video\2017-10-15\21hour\mp4\12.13.01-12.14.32[M][0#0][0].mp4
c:\video\2017-10-18\16hour\jpg\21.42.31[M][0#0][0].jpg
c:\video\2017-10-18\16hour\mp4\21.42.31-21.45.38[M][0#0][0].mp4
I want all the *.jpg & *.mp4 files to get moved to c:\video\ & also completely ditch the old name for 'date_time' + extension, similar to below:
-- after:
c:\video\2017-10-15_12.13.15.jpg
c:\video\2017-10-15_12.13.01-12.14.32.mp4
c:\video\2017-10-18_21.42.31.jpg
c:\video\2017-10-18_21.42.31-21.45.38.mp4
I discovered ForFiles, and was able to use the /s to recursively search all subfolders & move the *.mp4 & *.jpg to a single location:
forfiles /p c:\video\ /s /m *.mp4 /c "cmd /c move #PATH c:\video\"
forfiles /p c:\video\ /s /m *.jpg /c "cmd /c move #PATH c:\video\"
Now for the renaming, at this point I can almost taste victory as this gives me the desired filename output:
forfiles /p c:\video\ /s /m *.mp4 /c "cmd /c echo #FDATE_#FTIME.mp4"
forfiles /p c:\video\ /s /m *.jpg /c "cmd /c echo #FDATE_#FTIME.jpg"
But when I replace echo with either ren/rename/move, I get errors:
The system cannot find the path specified.
The syntax of the command is incorrect.
But the error only happens when I'm using #FDATE and/or #FTIME. These variables all work fine: #FNAME, #ISDIR, #FSIZE
forfiles /p c:\video\ /m *.mp4 /c "cmd /c ren #FILE #ISDIR.#EXT"
forfiles /p c:\video\ /m *.mp4 /c "cmd /c rename #FILE #FSIZE.#EXT"
forfiles /p c:\video\ /m *.mp4 /c "cmd /c move #FILE #EXT.#EXT"
Once I get the renaming part to work, I will combine both the move & rename into one command... But I can't figure out why #FDATE & #FTIME won't take, is there something wrong with my syntax, or is this just something ForFiles doesn't allow?
The problem explained
Firstly, replacing echo with just move or ren alone will not work because it will only detect one argument being passed to it. This could explain the syntax error.
Syntax of ren from Microsoft Docs
ren [<Drive>:][<Path>]<FileName1> <FileName2>
rename [<Drive>:][<Path>]<FileName1> <FileName2>
Syntax of move from Microsoft Docs
move [{/y | /-y}] [<Source>] [<Target>]
Secondly, the variables #FDATE and #FTIME are locale-dependent. Their format will depend on your device's regional formatting, e.g. #FDATE's format could contain / and #FTIME's format contains :. These are reserved characters and could result in an error if misused. This could explain the path error.
A solution using forfiles
If you want to do this with forfiles you will have to use the for command, like this. You can change the date format by extracting substrings like in this answer. You can change the time format by replacing the colon : with a dot ., explained here.
You could use /m once for .jpg and again for .mp4, but you could also try it like this for loop. This means you would have to nest the for in the example in another for loop.
According to the move /?, if you move just one file you can rename it as well.
Example:
setlocal EnableDelayedExpansion
for %%F in (.jpg, .mp4) do (
for /f "tokens=1,2,3,4 delims=|" %%A in (
'forfiles /p "C:\video" /s /m "*%%F" /c "cmd /c echo #PATH^|#EXT^|#FDATE^|#FTIME"'
) do (
set filepath=%%~A
set extension=%%~B
set datestamp=%%~C
set timestamp=%%~D
set parseddate=!datestamp:~6,4!-!datestamp:~3,2!-!datestamp:~0,2!
move "!filepath!" "C:\video\!parseddate!_!timestamp::=.!.!extension!"
)
)
endlocal
If the file count is large, it will take a little while for batch to finish. forfiles has to read the files first and the for loop has to also go through that file list. Also, forfiles is locale-dependent. If the regional date format of your device is 2020/02/10 then the indices of the substring in my example are not correct.
A solution using PowerShell
.NET Framework's LastWriteTime property is locale-independent, as in it does not dependent on your device's regional formatting. There is also a LastWriteTimeUtc property if you prefer to use UTC. You will still have to use a for loop inside a batch file though.
PowerShell's Get-ChildItem returns all children of the parent folder specified by the -Path parameter and with the -Recurse parameter you can do so recursively. If you want the result to include just .jpg and .mp4 files you should use -Include *.jpg, *.mp4. Normally when using -Include you would need to append \* to the path, but since -Recurse is used it's not necessary.
With Get-ItemProperty and the -Name parameter you can list the Fullname, LastWriteTime and Extension properties. To make it more readable for the for loop they have to be called like $var.Property and concatenated. ToString() must be used to convert LastWriteTime to string and change its format.
Example:
for /f "tokens=1,2 usebackq delims=|" %%A in (
`powershell -Command "& {Get-ChildItem -Path 'C:\video\*' -Recurse -Include *.jpg, *.mp4 | ForEach {Get-ItemProperty -Path $_.FullName -Name FullName, LastWriteTime, Extension} | ForEach {$_.FullName+'|'+$_.LastWriteTime.ToString('yyyy-MM-dd_HH.mm.ss')+$_.Extension}}"`
) do (
move "%%~A" "C:\video\%%~B"
)
Note: You can leave out -Path, but then the first argument must be the path.