Writing Batch Files on FreeDOS
A practical tour of FreeDOS batch scripting: variables, control flow, argument handling, and the quirks that differ from a modern shell.
Batch files are DOS’s native scripting mechanism, interpreted directly by COMMAND.COM — no separate scripting engine, no separate interpreter to invoke, just a .BAT file containing the same commands you’d type interactively, plus a small set of batch-specific control-flow directives.
The basics: a script is just commands
Any .BAT file is executed line by line, exactly as if you’d typed each line at the prompt yourself:
@ECHO OFF
ECHO Starting backup...
COPY C:\DATA\*.TXT A:\BACKUP\
ECHO Done.
@ECHO OFF suppresses DOS’s default behavior of echoing every command back to the screen before running it — the @ prefix on that specific line also suppresses the ECHO OFF command itself from being echoed, a small but universally-used idiom.
Variables and the environment
Batch files read and write environment variables — the same flat, inherited key-value store every DOS process has access to — using SET and %VARNAME% substitution:
SET NAME=daniel
ECHO Hello, %NAME%!
SET /A COUNT=COUNT+1
SET /A performs integer arithmetic, since batch variables are otherwise just text — there’s no native concept of a numeric type without that flag.
Command-line arguments: %1 through %9
A batch file’s own arguments are available as %1 through %9 (with %0 being the batch file’s own name), and SHIFT moves the argument window forward to handle more than nine:
@ECHO OFF
ECHO First argument: %1
ECHO Second argument: %2
:LOOP
IF "%1"=="" GOTO END
ECHO Processing: %1
SHIFT
GOTO LOOP
:END
Control flow: IF and GOTO, not real loops
Batch has no native for/while loop construct in the way a modern language does — repetition is built entirely from IF plus GOTO jumping to a :LABEL, as the argument-processing example above shows. IF supports string comparison, numeric comparison (with /I for case-insensitive), and existence checks:
IF EXIST C:\CONFIG.SYS ECHO Config found
IF NOT EXIST C:\TEMP MKDIR C:\TEMP
IF %ERRORLEVEL% NEQ 0 GOTO ERROR_HANDLER
ERRORLEVEL is DOS’s version of a shell exit status — every program that exits sets it, and IF ERRORLEVEL n tests for a value greater than or equal to n, which trips up people expecting exact-match semantics.
FOR: DOS’s actual iteration construct
DOS does have a genuine iteration command, just spelled differently than a modern language’s loop — FOR iterates over a set of files or a literal list:
FOR %%F IN (*.TXT) DO COPY %%F A:\BACKUP\
FOR %%N IN (1 2 3 4 5) DO ECHO Number: %%N
Note the doubled %% — inside a batch file, a single % in a FOR variable would be interpreted as the start of a variable substitution, so the loop variable is escaped by doubling it (this differs from using FOR interactively at the prompt, where a single % is correct).
Subroutines: CALL and labels
CALL invokes another label within the same batch file (or another batch file entirely) as a subroutine, returning control afterward — the closest batch equivalent to a function call:
@ECHO OFF
CALL :GREET
CALL :GREET
GOTO END
:GREET
ECHO Hello!
GOTO :EOF
:END
GOTO :EOF is the idiomatic way to “return” from a subroutine — jumping to the (virtual) end of the file rather than falling through into whatever code happens to follow.
Where batch scripting genuinely differs from a modern shell
Batch has no real data structures, no functions with parameters and return values (only global-environment-variable-based communication and ERRORLEVEL), and string manipulation is famously awkward — substring extraction and manipulation rely on obscure variable-expansion syntax (%VAR:~0,5% for a substring) rather than anything resembling a modern string library. That said, for its actual job — sequencing DOS commands, basic conditional logic, simple file operations — batch scripting is entirely sufficient, and its simplicity (interpreted directly by the shell already running, no separate runtime) is exactly why it’s remained usable, unchanged in spirit, for over four decades.