Input and Output
Writing output with echo and printf or Write-Output, reading input with read or Read-Host, and why Write-Host and the output stream behave differently.
Output in Bash: echo and printf
echo is the command everybody learns first (see
echo and printf), and for a fixed message it is fine. For anything else it has two
problems. It treats a first argument that looks like an option as an
option, so a variable that happens to contain -n prints
nothing. And its handling of backslashes differs between shells:
Bash needs -e for \t,
dash (/bin/sh on Debian and Ubuntu)
interprets it without.
option="-n"
echo "$option"
echo "---"
printf '%s\n' "$option"
echo -e "web01\tOK"
sh -c 'echo "web01\tOK"'
--- -n web01 OK web01 OK
printf is the command that prints text
according to a format string: placeholders such as
%s (string), %d (whole number) and
%.2f (number with two decimals) are filled with the
arguments that follow, and nothing is added that you did not ask
for, not even a newline. It is a Bash builtin and a standard
command, and it behaves the same in every shell, which is why POSIX
(the Portable Operating System Interface standard) itself recommends
it over echo for new scripts.
printf '%s kWh\n' 4521 312
printf '%-8s|%6d\n' M-1042 4521 M-1043 312
printf '%.2f EUR\n' 1265.879
printf 'INC-%05d\n' 42
printf -v ticket 'INC-%05d' 20419
echo "Ticket: $ticket"
4521 kWh 312 kWh M-1042 | 4521 M-1043 | 312 1265.88 EUR INC-00042 Ticket: INC-20419
-
If there are more arguments than placeholders,
printfrepeats the format: that is how one line prints a whole table. -
%-8spads to 8 characters, left-aligned;%6dpads a number to 6, right-aligned;%05dpads with zeros. -
printf -v name(Bash only) stores the result in a variable instead of printing it.
printf 'Meter %s: %d kWh\n' M-1042 abc
echo "exit status: $?"
printf 'Day %d\n' 08
echo "exit status: $?"
example.sh: line 1: printf: abc: invalid number Meter M-1042: 0 kWh exit status: 1 example.sh: line 3: printf: 08: invalid octal number Day 0 exit status: 1
Use %s for values you only display, and validate input
before you use it as a number (see
Reading input). In Bash arithmetic,
10#$day forces base 10.
Formatting in PowerShell
PowerShell's counterpart of printf is the format
operator -f: placeholders in braces, numbered from 0,
with an optional width (negative for left-aligned) and a format such
as N2 (number with thousands separators and two
decimals) or D5 (whole number, five digits).
'{0,-8}|{1,6}' -f 'M-1042', 4521
'{0,-8}|{1,6}' -f 'M-1043', 312
'{0:N2} EUR' -f 1265.879
'INC-{0:D5}' -f 42
'{0} has {1} open tickets' -f 'Amara', 3
M-1042 | 4521 M-1043 | 312 1,265.88 EUR INC-00042 Amara has 3 open tickets
Number and date formats follow the culture of the session. The
examples here run with the invariant culture, which writes
1,265.88; on a Dutch Windows machine the same line
prints 1.265,88. For files that other programs read,
use a fixed format and culture; for people, the local one is
friendlier.
Output in PowerShell
PowerShell has a rule that surprises everyone coming from Bash:
anything a line produces that is not captured or redirected is
output. A string on its own line, the result of a calculation, the
objects a cmdlet returns: all of it goes to the success stream, and
from there down the pipeline or, at the end, to the screen.
Write-Output is the cmdlet that
sends objects to that success stream explicitly (its alias is
echo); writing a bare value does the same.
The consequence: a function returns everything it outputs,
not just the value after return. A status message meant
for the user ends up in the result:
function Get-Usage {
"Checking disk"
91
}
$u = Get-Usage
"Captured $($u.Count) values: $($u -join ', ')"
function Get-Total {
Write-Host "Calculating..."
Write-Output 42
}
$t = Get-Total
"Total: $t"
Captured 2 values: Checking disk, 91 Calculating... Total: 42
Get-Usage was meant to return 91 and returned an array
of two values. Get-Total does it right.
Write-Host writes text for the
person at the screen: it is not part of the function's output, so
$t holds only 42, while "Calculating..." still appears.
Write-Host can also color text, which makes it the natural choice
for progress and status messages in interactive scripts.
In Bash the same split exists, but by convention: data goes to
standard output, and a message such as "Calculating..." belongs on
standard error (echo "Calculating..." >&2), so
that t=$(get_total) captures only the number.
Output streams
A Bash script has the three
standard streams
of every program: standard input (0), standard output (1) and
standard error (2). Results go to standard output, errors and
messages to standard error, so that a pipe or a redirection carries
only the data. Inside a script you send a line to standard error
with >&2:
check() {
echo "web01: OK"
echo "db01: no response" >&2
}
check > report.txt 2> errors.txt
echo "report.txt: $(cat report.txt)"
echo "errors.txt: $(cat errors.txt)"
check 2>/dev/null
report.txt: web01: OK errors.txt: db01: no response web01: OK
PowerShell has six numbered output streams, each with its own cmdlet, and the redirection operators use the same numbers:
| No. | Stream | Written by | Shown by default |
|---|---|---|---|
| 1 | Success | Bare values, Write-Output |
Yes (it is the output) |
| 2 | Error | Write-Error, failing cmdlets |
Yes |
| 3 | Warning | Write-Warning |
Yes, prefixed WARNING: |
| 4 | Verbose | Write-Verbose |
No; with -Verbose |
| 5 | Debug | Write-Debug |
No; with -Debug |
| 6 | Information |
Write-Information, Write-Host
|
Write-Host yes, Write-Information no |
The Information stream is
stream 6, added in PowerShell 5.0 for messages meant for the user
rather than for the pipeline. Since then Write-Host is
a wrapper around Write-Information that is always
displayed, which means its text can be suppressed with
6>$null or captured with 6>&1.
Write-Host "Starting check" 6>$null
Write-Host "Visible on screen"
Write-Information "Not shown by default"
Write-Information "Shown" -InformationAction Continue
Write-Verbose "Only with -Verbose"
Write-Verbose "Forced" -Verbose
Visible on screen Shown VERBOSE: Forced
Redirection works per stream, and *> takes all of
them. Because PowerShell streams carry objects, what you capture are
records, not lines of text:
function Test-Server {
"web01: OK"
Write-Warning "db01: slow response"
Write-Error "db01: no response"
}
Test-Server 2>$null 3>$null
"---"
$result = Test-Server *>&1
$result | ForEach-Object { $_.GetType().Name }
web01: OK --- String WarningRecord ErrorRecord
Reading input
Scripts that run unattended should take their input from arguments,
files or environment variables, never from a prompt. For interactive
tools, both shells can ask. In Bash,
read reads one line of
standard input
into variables; always give it -r so backslashes stay
as they are:
printf 'C:\\Logs\\new\n' > paths.txt
read line < paths.txt
echo "Without -r: $line"
read -r line < paths.txt
echo "With -r: $line"
Without -r: C:Logsnew With -r: C:\Logs\new
-p "Prompt: " shows a prompt, but only when the input
comes from a terminal. When the script is fed through a pipe or a
file, as in the next example, Bash shows no prompt at all, which is
what you want in automation. Everything a user types is untrusted
text, so the script checks the reading before calculating with it:
cat > ask.sh <<'X'
#!/bin/bash
read -rp "Name: " name
read -rp "Reading in kWh: " kwh
if [[ ! $kwh =~ ^[0-9]{1,6}$ ]]; then
echo "Not a whole number: '$kwh'" >&2
exit 2
fi
echo "Thanks, $name. Usage since the last reading: $(( 10#$kwh - 4171 )) kWh"
X
printf 'Amara\n4521\n' | bash ask.sh
printf 'Amara\nlots\n' | bash ask.sh
echo "exit status: $?"
Thanks, Amara. Usage since the last reading: 350 kWh Not a whole number: 'lots' exit status: 2
Read-Host is PowerShell's cmdlet for reading a line typed by the user, with an optional prompt to which it adds ": ". It always returns a string, whatever the user types. The same check in PowerShell, first with valid input, then with invalid input (the runner echoes the typed text after the prompt, as a console would):
$name = Read-Host "Name"
$kwh = Read-Host "Reading in kWh"
if ($kwh -notmatch '^\d{1,6}$') {
Write-Error "Not a whole number: '$kwh'"
exit 2
}
"Thanks, $name. Usage since the last reading: $([int]$kwh - 4171) kWh"
Name: Amara Reading in kWh: 4521 Thanks, Amara. Usage since the last reading: 350 kWh
$name = Read-Host "Name"
$kwh = Read-Host "Reading in kWh"
if ($kwh -notmatch '^\d{1,6}$') {
Write-Error "Not a whole number: '$kwh'"
exit 2
}
"Thanks, $name. Usage since the last reading: $([int]$kwh - 4171) kWh"
Name: Amara Reading in kWh: lots Write-Error: Not a whole number: 'lots'
Without the [int], $kwh - 4171 would still
work (the minus operator converts), but
$kwh + 350 would append text. In real PowerShell tools,
a mandatory parameter with a validation attribute is usually better
than Read-Host: PowerShell then prompts for a missing
value itself and rejects invalid ones before your code runs (see
Functions and Parameters).
read -rsp "API token: " token; echo
$token = Read-Host "API token" -AsSecureString
Writing output to files
Bash writes files with
redirection: > replaces the file,
>> appends, and tee writes to a file
and the screen at once.
echo "web01: OK" > report.txt
echo "db01: OK" >> report.txt
echo "app01: OK" | tee -a report.txt
echo "---"
cat report.txt
app01: OK --- web01: OK db01: OK app01: OK
PowerShell has the same operators, plus cmdlets:
Out-File writes what you would see on screen,
Set-Content and Add-Content write the
values as text, and Tee-Object works like
tee.
"web01: OK" | Out-File report.txt
"db01: OK" | Out-File report.txt -Append
Add-Content report.txt "app01: OK"
Get-Content report.txt
(Get-Content report.txt -AsByteStream)[0..2] -join ' '
web01: OK db01: OK app01: OK 119 101 98
The last line shows the first three bytes of the file: 119, 101, 98
are "w", "e", "b" in
UTF-8,
with no
byte order mark
in front. That is the default in PowerShell 7. Windows PowerShell
5.1 is different: its Out-File and
> write UTF-16 with a byte order mark, which
grep and many Linux tools read as text full of zero
bytes. When a file must be read by another program, choose the
encoding explicitly: Set-Content -Encoding utf8.
Side by side
| Task | Bash | PowerShell |
|---|---|---|
| Output a value |
printf '%s\n' "$x", echo "$x"
|
$x, Write-Output $x |
| Message for the user | echo "…" >&2 |
Write-Host, Write-Verbose |
| Error message |
echo "…" >&2 plus exit 1
|
Write-Error, throw |
| Warning | echo "WARNING: …" >&2 |
Write-Warning |
| Format a number | printf '%.2f' 3.14159 |
'{0:N2}' -f 3.14159 |
| Read a line | read -rp "Name: " name |
$name = Read-Host "Name" |
| Write / append a file |
> / >>,
tee -a
|
Set-Content / Add-Content,
Tee-Object
|
| Discard output | >/dev/null 2>&1 |
*>$null, | Out-Null |