skip to content

echo — Output Text and Control Command Echo

Print text to stdout, expand environment variables, write blank lines, redirect output to files, and toggle command-echoing in cmd.exe batch scripts.

17 min read 115 snippets deep dive

echo — Output Text and Control Command Echo#

What it is#

echo is a built-in cmd.exe command with two distinct roles: (1) it prints a string to standard output, expanding %VARIABLE% references along the way; and (2) it controls whether cmd.exe prints each command before running it (ECHO ON) or suppresses that display (ECHO OFF). Every batch script starts its life with @echo off to silence the echoed commands — the @ prefix suppresses the echo of that specific line before the global setting takes effect.

Availability#

echo is built into cmd.exe on every Windows version. PowerShell equivalent: Write-Output (aliased echo).

echo /?

Output:

Displays messages, or turns command-echoing on or off.

ECHO [ON | OFF]
ECHO [message]

Type ECHO without parameters to display the current echo setting.

Syntax#

echo takes an optional ON/OFF keyword or any message text. No quotes are needed — everything after the space is printed verbatim.

echo [ON | OFF | message]

Output: (the message or ON/OFF status)

Basic text output#

Anything after echo (with a space) is sent to stdout. Environment variables are expanded before printing.

echo Hello, world!

Output:

Hello, world!
echo Current user: %USERNAME%

Output:

Current user: alicedev
echo Today is %DATE% and the time is %TIME%

Output:

Today is Mon 04/28/2026 and the time is 14:22:05.31

Blank lines with echo.#

echo followed by a period (no space) prints an empty line. echo alone prints the current ON/OFF state — not a blank line.

echo Line one
echo.
echo Line three

Output:

Line one

Line three
rem Checking the current echo state
echo

Output:

ECHO is on.

ECHO ON and ECHO OFF#

echo off suppresses the display of subsequent commands; echo on restores it. Prefix any single command with @ to suppress only that line’s echo without changing the global state.

@echo off
echo This line is printed.
echo And this one too.

Output:

This line is printed.
And this one too.

With echo on, each command appears before its output:

echo on
echo Hello

Output:

C:\>echo Hello
Hello

Redirecting echo to a file#

Redirect echo output with > (overwrite) or >> (append) to build text files without an editor.

echo [INFO] Build started > build.log
echo [INFO] Compiling src\main.c >> build.log
type build.log

Output:

[INFO] Build started
[INFO] Compiling src\main.c
rem Write a blank line as a separator in a log file
echo. >> build.log
echo [INFO] Done. >> build.log

Output: (none — written to file)

Writing multi-line content with parenthesised blocks#

Wrap a series of echo statements in a parenthesised block and redirect once to write a multi-line file efficiently.

(
    echo [section]
    echo key=value
    echo other=42
) > config.ini
type config.ini

Output:

[section]
key=value
other=42

Echoing special characters#

Some characters (<, >, |, &, ^) are interpreted by cmd.exe. Escape them with ^ or enclose the entire string in quotes (quotes are then printed literally if not escaped).

echo Redirect: ^> and pipe: ^|

Output:

Redirect: > and pipe: |
rem Ampersand without escape would run two commands
echo Q^&A section

Output:

Q&A section

Suppressing the trailing newline — echo with NUL#

There is no built-in flag to suppress the trailing \r\n. The common workaround is set /P with an empty string, which outputs the prompt text without a newline.

<NUL set /P DUMMY=Enter value: 

Output:

Enter value: 

(cursor stays on same line — useful before reading user input)

Common pitfalls#

  1. echo alone shows ON/OFF state — use echo. (no space) for a blank line, not plain echo.
  2. Trailing space in echo message — the space before the newline is part of the output; it is invisible on screen but can break file comparisons.
  3. Special characters need ^ escapingecho a > b redirects instead of printing a > b; write echo a ^> b.
  4. @echo off must be the first line — placing it anywhere else still echoes all preceding commands.
  5. Quotes are printed literallyecho "hello" outputs "hello" with quotes included; there is no auto-stripping.

Real-world recipes#

@echo off
echo ============================================================
echo  Build Pipeline — %DATE%
echo ============================================================
echo.
echo Step 1: Cleaning output directories...

Output:

============================================================
 Build Pipeline — Mon 04/28/2026
============================================================

Step 1: Cleaning output directories...

Create a minimal .env file from batch#

@echo off
(
    echo APP_ENV=production
    echo LOG_LEVEL=warn
    echo PORT=8080
) > .env
echo .env written.

Output:

.env written.

Conditional status message#

@echo off
if exist C:\Logs\app.log (
    echo [OK] Log file found.
) else (
    echo [WARN] Log file missing — check the service.
)

Output:

[WARN] Log file missing — check the service.

Append a timestamped entry to a log#

echo [%DATE% %TIME%] Deployment complete >> C:\Logs\deploy.log
type C:\Logs\deploy.log

Output:

[Mon 04/28/2026 14:30:01.12] Deployment complete

echo and the dual identity#

echo looks like a single command but is really two distinct features sharing one keyword. Form 1 — echo <message> — emits text. Form 2 — echo ON|OFF — toggles command echoing for the current cmd.exe interpreter. The distinction matters when you write echo on thinking you are printing the word “on” — you are actually changing a shell setting. Use echo. on (with the trailing period) or quote tricks to print the literal words ON and OFF.

rem This toggles command echoing, does NOT print "on"
echo on

Output:

(command echoing turned on; no text printed)
rem This prints the literal word "on"
echo.on

Output:

on
echo "on"

Output:

"on"

Internal command — no .exe#

echo is implemented inside cmd.exe, not a separate executable. You cannot find it with where echo, you cannot call it directly with CreateProcess, and you cannot replace it with a custom binary by putting one earlier in PATH. The only way to invoke it is through a cmd.exe interpreter (either interactively or via cmd /c).

where echo

Output:

INFO: Could not find files for the given pattern(s).
rem To use echo from outside cmd, wrap with cmd /c
cmd /c "echo Hello from cmd"

Output:

Hello from cmd

All forms of echo#

FormEffect
echoShow current ECHO ON / OFF state
echo <text>Print <text> to stdout with trailing CRLF
echo.Print an empty line (CRLF only)
echo. <text>Same as echo <text>
echo onRe-enable command echoing
echo offDisable command echoing
@echo <text>Print <text> without echoing the echo command itself
@echo offDisable echoing globally and suppress this command’s own echo
echo <text> > fileRedirect to a file (overwrite)
echo <text> >> fileAppend to a file
`echo cmd`

The @ prefix in batch scripts#

Without @, every command in a batch script is printed before it runs (because ECHO defaults to ON). The @ symbol suppresses the echo of that one line. Almost every batch script starts with @echo off — the @ hides the echo off command itself; thereafter, no commands are echoed.

echo off
echo Hello

Output:

C:\>echo off
Hello

(echo off was itself echoed before it took effect)

@echo off
echo Hello

Output:

Hello

(@echo off was suppressed; subsequent commands also suppressed by the global state)

For mixed scripts where you want most commands echoed but a few hidden, sprinkle @ per line:

echo ON
echo Step 1
@rem this comment is hidden
echo Step 2

Output:

C:\>echo Step 1
Step 1

C:\>echo Step 2
Step 2

Writing characters that cmd.exe interprets#

cmd.exe reserves several characters: <, >, |, &, ^, (, ), %, !. To emit them literally, escape them with the caret ^ or quote the entire argument. The caret is consumed during parsing — it does not appear in output.

echo Pipe: ^| and amp: ^&

Output:

Pipe: | and amp: &
echo Parens: ^(text^) and equals signs: 1+1^=2

Output:

Parens: (text) and equals signs: 1+1=2
rem Percent sign — double it to escape
echo Loading... 50%%

Output:

Loading... 50%
rem Inside delayed-expansion blocks, escape ! as well
setlocal enabledelayedexpansion
echo Hello^^!

Output:

Hello!

Printing leading whitespace#

cmd.exe trims one space after the echo keyword. To print a line that starts with extra leading whitespace, use echo: or echo. followed immediately by the spaces, or use parentheses.

echo     four leading spaces

Output:

    four leading spaces

(works because the visible spaces survive; only the one space after echo is trimmed)

echo:    four leading spaces

Output:

    four leading spaces
(
    echo     indented line
) > out.txt
type out.txt

Output:

    indented line

echo. alternatives and quirks#

echo. (with a period) is the canonical way to print a blank line. Other characters work too but behave slightly differently:

FormResultNotes
echo.Blank lineCanonical — works everywhere
echo:Blank lineSame as echo.
echo;Blank lineWorks in cmd.exe
echo/Blank lineWorks but rare
echo\Blank lineWorks
echo,Blank lineWorks
echo”ECHO is on/off.”NOT a blank line — prints state

The fastest-parsing form is echo. because the period is non-alphabetic and cmd can short-circuit the lookup. In modern Windows the speed difference is microseconds, but in tight loops the choice still matters for legibility.

echo Line 1
echo.
echo Line 3
echo:
echo Line 5

Output:

Line 1

Line 3

Line 5

Delayed expansion and !variable!#

%VAR% expansion happens once when cmd.exe parses a line. Inside a parenthesised block (for, if, (...)) the expansion freezes at parse time, which surprises scripts that modify variables inside the block. Enabling delayed expansion with setlocal enabledelayedexpansion lets you use !VAR! for runtime expansion.

@echo off
set N=0
for %%i in (a b c) do (
    set /A N+=1
    echo Step %N% of 3 — file %%i
)

Output:

Step 0 of 3 — file a
Step 0 of 3 — file b
Step 0 of 3 — file c

(broken — %N% is captured at parse time, before the loop runs)

@echo off
setlocal enabledelayedexpansion
set N=0
for %%i in (a b c) do (
    set /A N+=1
    echo Step !N! of 3 — file %%i
)

Output:

Step 1 of 3 — file a
Step 2 of 3 — file b
Step 3 of 3 — file c

Echoing to stderr instead of stdout#

echo always writes to stdout (handle 1). To send a message to stderr, redirect with 1>&2:

echo Error: file not found 1>&2

Output:

Error: file not found

(visible on screen, but echo ... 1>&2 | more would not pipe — stderr bypasses pipes)

rem Useful pattern: errors to stderr, normal output to stdout
echo Processing file...
echo WARNING: skipping bad row 1>&2
echo Done.

Output:

Processing file...
WARNING: skipping bad row
Done.

When redirecting both, separate them with > stdout.log 2> stderr.log. To merge stderr into stdout use 2>&1 after the primary redirect.

script.bat > combined.log 2>&1

Output: (none — all output captured in combined.log)

Echoing a CRLF vs LF#

cmd.exe always terminates echo output with CRLF (\r\n, 0x0D 0x0A). There is no built-in flag to emit Unix-style LF only. If you need LF line endings (e.g. for shell scripts, JSON, .env files), pipe through find/findstr with a transform, or generate the file in PowerShell with the [char]10 newline.

rem CRLF is fine for Windows tools
echo line1>script.sh
echo line2>>script.sh

Output: (none — exits 0 on success)

# LF-only output via PowerShell
[IO.File]::WriteAllText("script.sh", "line1`nline2`n")

Output: (file created with Unix LF terminators)

PowerShell equivalents — Write-Output, Write-Host, echo#

PowerShell has three commands that overlap with echo. They are not interchangeable — they differ in what they write to, whether they participate in the pipeline, and whether output can be captured.

CmdletGoes toCaptured by =?Pipeable?Alias
Write-OutputSuccess stream (stdout-equivalent)YesYesecho, write
Write-HostConsole hostNoNo
Write-InformationInformation stream (PS 5+)Yes (with -IV)No
Write-VerboseVerbose streamOnly via $VerbosePreferenceNo
Write-WarningWarning streamYes (with -WV)No
Write-ErrorError stream (stderr-equivalent)Yes (in $Error)No
# Write-Output — pipeable, captured, goes through pipeline
Write-Output "Hello"
echo "Hello"           # alias of Write-Output
"Hello"                # implicit Write-Output

Output:

Hello
Hello
Hello
# Write-Host — bypasses pipeline, writes to console only
Write-Host "Hello" -ForegroundColor Green

Output:

Hello   (green)
# Difference in capture
$captured = Write-Output "captured"
Write-Host "captured value: $captured"

$captured2 = Write-Host "not-captured"   # $captured2 is $null
Write-Host "captured2 value: '$captured2'"

Output:

captured value: captured
not-captured
captured2 value: ''

echo in PowerShell is exactly Write-Output — no special behaviour. Anyone migrating batch scripts can substitute echo 1:1 but should be aware that the alias does not control echoing the way @echo off does in cmd.

Write-Host colours and formatting#

Write-Host is the right choice for batch-script-style informational output that should never leak into pipelines. It supports per-call colours and -NoNewline.

Write-Host "[" -NoNewline
Write-Host "OK" -ForegroundColor Green -NoNewline
Write-Host "] Service started"

Output:

[OK] Service started
Write-Host "Warning" -ForegroundColor Yellow -BackgroundColor Black
Write-Host "Error"   -ForegroundColor Red
Write-Host "Info"    -ForegroundColor Cyan

Output: (each line in the specified colour)

Write-Output and the pipeline#

Write-Output emits an object into the success stream. Strings, numbers, hashtables, custom objects — all go through Write-Output. The implicit return from a function is also a Write-Output. This is why scripts that mix Write-Host and Write-Output produce surprising results when redirected: only the Write-Output parts are captured.

function Get-Status {
    Write-Host "Running checks..."        # to console only
    Write-Output "OK"                     # to pipeline
}

$result = Get-Status
Write-Host "Captured: $result"

Output:

Running checks...
Captured: OK

If you replace Write-Output with Write-Host, $result becomes $null.

Cmd echo vs > quirks#

A trailing space before > is included in the file. Quoting helps but is not always sufficient.

echo hello >file1.txt

Output (file1.txt contents):

hello 

(note the trailing space)

echo hello>file2.txt

Output (file2.txt contents):

hello

(no trailing space — > is adjacent to o)

For zero-byte file creation:

rem Wrong — creates a file with "\r\n"
echo > empty1.txt
rem Right — creates a true empty file
type nul > empty2.txt
copy nul empty3.txt

Output:

        0 file(s) copied.

Numeric prefixes for redirection#

cmd.exe redirection has explicit handle numbers when needed:

TokenMeans
> or 1>Redirect stdout
2>Redirect stderr
< or 0<Read stdin from
2>&1Merge stderr into stdout
1>&2Merge stdout into stderr
>>Append (stdout)
2>>Append (stderr)
>nulDiscard stdout
2>nulDiscard stderr
>nul 2>&1Discard everything
rem Capture all output, errors and all, into one file
build.bat > build.log 2>&1

Output: (none — all output redirected to build.log)

rem Discard noisy output, keep only errors
build.bat > nul

Output:

error C2065: 'undefined_var': undeclared identifier
rem Discard errors, keep stdout
build.bat 2> nul

Output: (varies)

echo in scheduled tasks and services#

echo in a script that runs under Task Scheduler with “Run whether user is logged on or not” has no visible console — anything echo’d disappears unless redirected to a file. Always redirect outputs explicitly in scheduled scripts:

@echo off
echo [%DATE% %TIME%] Task started >> C:\Logs\task.log
... task work ...
echo [%DATE% %TIME%] Task finished >> C:\Logs\task.log

Output: (none — all output redirected to task.log)

In PowerShell scheduled tasks, prefer Start-Transcript to capture all console output:

Start-Transcript -Path "C:\Logs\task-$(Get-Date -Format yyyyMMdd).log" -Append
Write-Host "Task started"
# ...
Stop-Transcript

Output: (everything written to the transcript file)

Locale and code page issues#

echo emits bytes in the current console code page (usually 437 or 850 on US Windows, 1252 in many western locales, UTF-8 on Windows 10/11 with chcp 65001). Non-ASCII text written with echo may be mojibake’d when read by another tool that assumes a different encoding.

chcp 65001
echo Café — naïve
> note.txt

Output:

Active code page: 65001
Café — naïve

For UTF-8 output, set the console to code page 65001 and ensure the .bat file itself is saved as UTF-8 without BOM (a BOM at the start of the file confuses cmd.exe).

# PowerShell — UTF-8 by default in 7+, explicit in 5.1
"Café — naïve" | Out-File -Encoding utf8 note.txt

Output: (file written in UTF-8)

Common pitfalls (continued)#

  1. echo on toggles state, doesn’t print “on”echo.on (no space) is the form that prints the literal word.
  2. Caret escaping is consumed once — to emit a literal caret, double it: echo a ^^ b prints a ^ b.
  3. Trailing CRLF cannot be removedecho always terminates with CRLF; for no newline use <NUL set /P =text.
  4. Variable expansion happens once per line — inside (...) blocks, switch to delayed expansion with setlocal enabledelayedexpansion and !VAR!.
  5. PowerShell echo is Write-Output, not Write-Host — output goes through the pipeline, can be captured, and is silent in >nul-style redirects only via Out-Null.
  6. %PATH% and other long values may exceed line lengthecho %PATH% can fail or truncate if %PATH% plus the trailing CRLF exceeds 8191 characters; split with set PATH | findstr Path.

Real-world recipes (continued)#

Self-documenting batch wrapper#

A wrapper script that prints what it is about to do, runs it, and reports success — using echo for narration.

@echo off
echo ----------------------------------------
echo  Step 1: Sync repository
echo ----------------------------------------
git pull
if errorlevel 1 (
    echo [FAIL] git pull failed.
    exit /b 1
)
echo [OK] Repository synced.

echo.
echo ----------------------------------------
echo  Step 2: Build
echo ----------------------------------------
npm run build
if errorlevel 1 (
    echo [FAIL] build failed.
    exit /b 1
)
echo [OK] Build complete.

Output:

----------------------------------------
 Step 1: Sync repository
----------------------------------------
[OK] Repository synced.

----------------------------------------
 Step 2: Build
----------------------------------------
[OK] Build complete.

Combine echo with tasklist/findstr to print a snapshot of relevant processes.

@echo off
echo Running processes containing "node":
tasklist /FI "IMAGENAME eq node.exe" /NH /FO TABLE
echo.
echo Running processes containing "python":
tasklist /FI "IMAGENAME eq python.exe" /NH /FO TABLE

Output:

Running processes containing "node":
node.exe                      1234 Console                    1     56,432 K
Running processes containing "python":
python.exe                    5678 Console                    1     32,108 K

Mixed cmd and PowerShell logging#

When a batch script invokes PowerShell for one task, route both outputs to the same log via >>:

@echo off
set LOG=C:\Logs\deploy_%DATE:~-4,4%%DATE:~-10,2%%DATE:~-7,2%.log
echo [%TIME%] Starting deployment >> %LOG%
powershell -NoProfile -Command "Get-Service | Where-Object Status -eq Running | Measure-Object | ForEach-Object Count" >> %LOG% 2>&1
echo [%TIME%] Done >> %LOG%
type %LOG%

Output (deploy_20260525.log):

[14:22:05.34] Starting deployment
38
[14:22:08.11] Done

PowerShell echo-equivalent with colours and timestamps#

A reusable function for coloured, timestamped status output.

function Write-Stamp {
    param(
        [string]$Message,
        [ValidateSet('Info','Warn','Error','OK')] [string]$Level = 'Info'
    )
    $ts = Get-Date -Format 'HH:mm:ss'
    $colour = switch ($Level) {
        'Info'  { 'Cyan' }
        'Warn'  { 'Yellow' }
        'Error' { 'Red' }
        'OK'    { 'Green' }
    }
    Write-Host "[$ts] [$Level] $Message" -ForegroundColor $colour
}

Write-Stamp "Sync started"
Write-Stamp "Cache miss" -Level Warn
Write-Stamp "Done"       -Level OK

Output:

[14:22:05] [Info]  Sync started
[14:22:06] [Warn]  Cache miss
[14:22:09] [OK]    Done

Generating a CSV header + rows#

echo with > for the header line and >> for rows is the simplest way to assemble a small CSV without a tool.

@echo off
echo Date,Hostname,Status > status.csv
echo %DATE%,%COMPUTERNAME%,Online >> status.csv
echo %DATE%,SRV01,Online >> status.csv
echo %DATE%,SRV02,Offline >> status.csv
type status.csv

Output:

Date,Hostname,Status
Mon 05/25/2026,MYHOST,Online
Mon 05/25/2026,SRV01,Online
Mon 05/25/2026,SRV02,Offline

Sources#

References consulted while writing this article. Links open in a new tab.

See also#