10. Capstone: File Organizer¶
The goal¶
Build organize.bat, a script that sorts a messy folder into subfolders by file type:
turns
into
Downloads\
Documents\ invoice.pdf notes.txt
Images\ cat.jpg cat2.png
Installers\ setup.exe
Archives\ archive.zip
Other\ (anything unrecognised)
It must use everything from this level: arguments, if, for, file ops, subroutines, redirection, exit codes.
Requirements¶
- Take the target folder as
%1. If missing or not a folder, print usage andexit /b 1. - Support a
--dry-runflag (in any position) that prints what would happen without moving anything. - Categorise by extension:
- Images:
.jpg .jpeg .png .gif .bmp .webp .heic - Documents:
.pdf .txt .md .doc .docx .xls .xlsx .ppt .pptx .csv - Archives:
.zip .rar .7z .tar .gz - Installers:
.exe .msi - anything else →
Other - Create category subfolders only when needed.
- Never move the script itself, and never recurse into the category folders you create.
- Write a log (
organize-log.txtin the target folder) of every move, with a timestamp. - Print a summary: how many files moved into each category.
- Exit
0on success, non-zero if any move failed.
Starter skeleton¶
@echo off
setlocal EnableExtensions EnableDelayedExpansion
set "TARGET="
set "DRYRUN="
rem --- parse arguments ---
:parse
if "%~1"=="" goto parsed
if /i "%~1"=="--dry-run" (
set "DRYRUN=1"
) else (
set "TARGET=%~1"
)
shift
goto parse
:parsed
if not defined TARGET (
echo Usage: %~nx0 ^<folder^> [--dry-run]
exit /b 1
)
if not exist "%TARGET%\" (
echo ERROR: not a folder: "%TARGET%" 1>&2
exit /b 1
)
set "LOG=%TARGET%\organize-log.txt"
set "FAILED=0"
set /a nImages=0, nDocs=0, nArch=0, nInst=0, nOther=0
rem --- main loop: files in the top level only ---
for %%F in ("%TARGET%\*") do (
if /i not "%%~fF"=="%~f0" (
call :categorize "%%~xF" CATEGORY
call :move_file "%%~fF" "!CATEGORY!"
)
)
rem --- summary ---
echo.
echo Images:%nImages% Documents:%nDocs% Archives:%nArch% Installers:%nInst% Other:%nOther%
if %FAILED% gtr 0 ( echo %FAILED% move(s) failed. & exit /b 1 )
exit /b 0
rem ===================================================================
:categorize
rem %1 = extension (with dot), %2 = out var name
set "_ext=%~1"
set "_cat=Other"
for %%e in (.jpg .jpeg .png .gif .bmp .webp .heic) do if /i "%_ext%"=="%%e" set "_cat=Images"
for %%e in (.pdf .txt .md .doc .docx .xls .xlsx .ppt .pptx .csv) do if /i "%_ext%"=="%%e" set "_cat=Documents"
for %%e in (.zip .rar .7z .tar .gz) do if /i "%_ext%"=="%%e" set "_cat=Archives"
for %%e in (.exe .msi) do if /i "%_ext%"=="%%e" set "_cat=Installers"
set "%~2=!_cat!"
goto :eof
:move_file
rem %1 = full source path, %2 = category name
set "_src=%~1"
set "_cat=%~2"
set "_dstdir=%TARGET%\%_cat%"
if defined DRYRUN (
echo [dry-run] "%~nx1" -> %_cat%\
) else (
if not exist "%_dstdir%\" md "%_dstdir%"
move /y "%_src%" "%_dstdir%\" >nul
if errorlevel 1 (
echo [%date% %time%] FAILED "%~nx1" -> %_cat%>>"%LOG%"
set /a FAILED+=1
) else (
echo [%date% %time%] moved "%~nx1" -> %_cat%>>"%LOG%"
)
)
if /i "%_cat%"=="Images" set /a nImages+=1
if /i "%_cat%"=="Documents" set /a nDocs+=1
if /i "%_cat%"=="Archives" set /a nArch+=1
if /i "%_cat%"=="Installers" set /a nInst+=1
if /i "%_cat%"=="Other" set /a nOther+=1
goto :eof
Why the tricky bits are there¶
EnableDelayedExpansion+!CATEGORY!—CATEGORYis set by a subroutine and read inside the sameforblock;%CATEGORY%would be stale (Intermediate Lesson 1).if /i not "%%~fF"=="%~f0"— skip the script itself so it doesn't moveorganize.batintoOther.for %%F in ("%TARGET%\*")— top level only; it does not descend intoImages\etc., so re-running is safe.move /y ... >nulthenif errorlevel 1— detect a failed move (file locked, permissions) and count it.- Counters (
nImagesetc.) are set with%%~xFcategorisation and read at the end where%var%is correct.
Test plan¶
- Make a scratch folder with one file of each type plus a weird one (
data.xyz). - Run with
--dry-run. Confirm the plan looks right and nothing moved, no log written. - Run for real. Check the subfolders, the summary counts, and
organize-log.txt. - Run again. It should find nothing to do (all files are now inside category folders, which the top-level glob skips) and still exit
0. - Lock a file (open it in an app) and run again with a new file of that type — confirm the failure is logged and the exit code is non-zero.
- Run with no argument, and with a path that doesn't exist — confirm usage message and
exit /b 1.
Extensions (do at least two)¶
--undo: readorganize-log.txtand move every "moved" file back to%TARGET%.--by-date: instead of type, sort intoYYYY-MMfolders using%%~tF.- Config file: read extension→category mappings from
organize.ininext to the script instead of hard-coding them (Intermediate Lesson 3). - Collision handling: if
Images\cat.jpgalready exists, rename the incoming file tocat (2).jpg. --quiet/--verboseoutput levels.
What you should be able to do now¶
- Explain every line, including why delayed expansion is required and where
%var%is still fine. - Add a new category without breaking anything.
- Predict the exit code for any input.
If all three are true, go to Intermediate.