env and printenv

Read the environment, and change it for one command

Updated 2026-09-18

printenv prints what a process was handed. env prints the same thing, but it is more often used for its other job: running a command with an environment you have changed, for that command and nothing else.

Both are programs on disk rather than shell builtins, so both see only what was exported. A shell variable that has never been exported is visible to the shell but is invisible to anything it starts, so printenv GREETING can print nothing and still have answered the question. Environment variables and PATH describes the model the rest of this follows from.

env NAME=value command and the shell's own NAME=value command prefix do the same thing. While the prefix is shorter, env is needed where the prefix cannot go: after sudo, which builds a fresh environment and ignores assignments made in front of it; on a shebang line, where the kernel allows one program and one argument; and with -i or -u, which take variables away rather than adding them. -i starts the command from an empty environment, close to what cron or a systemd unit hands a job. Reproducing that at your own prompt is most of the diagnosis when a script works as you type it and fails on a schedule.

Prefer printenv NAME to echo "$NAME" when the value itself is the question. Unquoted, echo $PATTERN is expanded by the shell before echo runs, so a value containing * comes back as a list of filenames; printenv is a separate process and hands back what is stored. Its exit status also separates a variable set to nothing from one that was never set, which echo has no way to report.

Both of the common surprises come from the shell running first. env GREETING=hello echo "$GREETING" prints an empty line, because $GREETING was expanded before env existed. And env execs a real program, so it cannot run a builtin, a function or an alias: there is no file called cd for it to execute, so env cd /tmp reports that cd was not found.

Sample files used on this page

Every example below was run against these files. Recreate them to follow along.

the working directory three files, so the shell has a pattern to expand and wc has something to count

accents.txt
notes.txt
report.txt

accents.txt the é is one character and two bytes, so wc -m answers twice

héllo

~/tools/greet a command that is not on PATH: Debian's stock ~/.profile adds ~/bin and ~/.local/bin under your home

#!/bin/sh
echo "hello from greet"
49 outputs, collapsed by default

Reading the environment

printenv prints values, env with no command prints the NAME=value pairs themselves, but neither of them sees a variable the shell has not exported. Plain env on a desktop machine runs to dozens of lines, several of them belonging to the session manager, so everything below extracts what it wants by name.

Print one variable

printenv HOME

Just the value, with no name and no quoting around it, so it drops straight into a script.

Show output
/home/user

Print several variables at once

printenv HOME PATH

One line per name, in the order asked for. The values are not labelled, so a script reading this has to count lines instead of matching names.

Show output
/home/user
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games

Print a variable with its name attached

env | grep '^PATH='

env prints the NAME=value form the environment is stored in. With no command after it, it prints every variable, and the grep narrows that to the one line we want here.

Show output
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games

Read PATH as the list it is

printenv PATH | tr ':' '\n'

Search order, first directory at the top. This is the first thing to look at when a program you can see is reported as not found, and which vs type vs command -v is the next.

Show output
/usr/local/sbin
/usr/local/bin
/usr/sbin
/usr/bin
/sbin
/bin
/usr/games

Find out whether a variable is set

printenv EDITOR; echo "exit: $?"

Exit status 1 with no output when the name is not in the environment. You can test for it with if printenv EDITOR >/dev/null; then.

Show output
exit: 1

Tell an empty variable from a missing one

export TIPS_EMPTY=
printenv TIPS_MISSING; echo "missing: $?"
printenv TIPS_EMPTY; echo "empty: $?"

A variable set to nothing is in the environment, so printenv prints its empty value and exits 0. echo "$VAR" prints an empty line for both cases and cannot tell you which you have.

Show output
missing: 1

empty: 0

See a value the shell has not rewritten

export PATTERN='*.txt'
echo $PATTERN
printenv PATTERN

Unquoted, echo $PATTERN hands the shell a pattern which it then expands. printenv is a separate process that never sees the pattern as one, so it prints what was stored.

Show output
accents.txt notes.txt report.txt
*.txt

Keep the spaces inside a value

export MESSAGE='two  spaces'
echo $MESSAGE
printenv MESSAGE

The same fault with no wildcard in sight: unquoted, the shell splits the value into words and echo rejoins them with one space each. Quoting the expansion fixes echo, while printenv works just fine, printing the string exactly.

Show output
two spaces
two  spaces

A variable the shell kept to itself

GREETING=hello
printenv GREETING
echo "exit: $?"

An assignment with no export makes a shell variable, which belongs to the shell and is not in the block handed to anything it starts. printenv is one of those things.

Show output
exit: 1

Export it, and it appears

GREETING=hello
export GREETING
printenv GREETING

export moves an existing variable into the environment. export GREETING=hello does both steps at once and is what you would normally write.

Show output
hello

Separate the values with NUL instead of newline

export TIPS_A=1 TIPS_B=2
printenv -0 TIPS_A TIPS_B | od -c

-0 ends each value with a NUL byte instead of a newline, which is what a value containing a newline of its own needs. od -c prints the bytes one at a time, because a NUL is invisible on a terminal and the point here is where the separators fall. xargs -0 and sort -z read this form.

Show output
0000000   1  \0   2  \0
0000004

A value with a newline in it, printed plainly

env -i "TIPS_NOTE=first line
second line" TIPS_AFTER=1 env

Three lines out of two variables, and the output gives no way to tell which of the three starts a new one. That ambiguity is what -0 removes.

Show output
TIPS_NOTE=first line
second line
TIPS_AFTER=1

Running one command with the environment changed

env NAME=value command sets the variable only in the copy of the environment the command receives, so the shell you typed it in is unaffected. The examples run sh -c instead of echo, because the shell expands a $NAME before env ever runs, which the third example shows.

Set a variable for one command

env GREETING=hello sh -c 'echo $GREETING'

env sets GREETING, execs sh, and takes no further part. The shell you typed this in is unchanged.

Show output
hello

The shell does this without env

GREETING=hello sh -c 'echo $GREETING'
printenv GREETING; echo "after: $?"

An assignment in front of a command is shell syntax with the same effect, with the benefit of being slightly shorter. env is for the places this cannot reach: after sudo, on a shebang line, and anywhere a variable has to be removed rather than set.

Show output
hello
after: 1

Why the new value does not reach echo

env GREETING=hello echo "[$GREETING]"

The shell expands $GREETING while building the command line, long before env runs and sets anything. Quoting does not help, since the expansion is the problem. Putting the command inside sh -c does help, because that inner shell does its expanding after env has finished.

Show output
[]

Set two variables at once

env EDITOR=nano PAGER=cat sh -c 'echo $EDITOR $PAGER'

Assignments are read until the first argument that isn't one. That argument is the command to be run. So a command whose own name contains an = cannot be run this way.

Show output
nano cat

The last assignment to a name wins

env GREETING=hello GREETING=goodbye sh -c 'echo $GREETING'

No warning about the first one. Worth knowing when the assignments come from a generated command line or from $@.

Show output
goodbye

A value that contains an equals sign

env URL=host=deb1 sh -c 'echo $URL'

Only the first = separates the name from the value, so no quoting is needed here. A variable name cannot contain one at all.

Show output
host=deb1

Run a command in another time zone

env TZ=Asia/Tokyo date -d '2026-06-01 09:00 UTC' +'%F %T %Z'
date -d '2026-06-01 09:00 UTC' +'%F %T %Z'

TZ is read by the C library underneath date, which is why setting it for this command works in the first place. The second line is the same instant with the machine's own time zone.

Show output
2026-06-01 18:00:00 JST
2026-06-01 09:00:00 UTC

Change what the tools count as a character

env LC_ALL=C wc -m < accents.txt
env LC_ALL=C.UTF-8 wc -m < accents.txt

wc -m counts characters, and LC_ALL decides which bytes make one. The same file is seven bytes but only six characters. LC_ALL=C is worth setting deliberately in a script whose output is parsed by another program.

Show output
7
6

The variable is gone once the command ends

env TIPS_TOKEN=secret true
printenv TIPS_TOKEN
echo "exit: $?"

env set TIPS_TOKEN in the environment it handed to true, and printenv runs afterwards in the original shell, which never had it: hence exit 1. A process passes its environment down to whatever it starts, and a child has no way to hand one back up, which is why an installer tells you to source its script instead of running it.

Show output
exit: 1

Taking variables away

-u removes one name, -i starts from nothing at all. Between them they answer most of "it works when I type it but it doesn't work from cron", by letting you run the command the way cron will.

Remove one variable

env -u HOME sh -c 'echo [${HOME-unset}]'

-u unsets the name rather than setting it empty, which are two different things: ${HOME-unset} supplies the unset default only for a name that is genuinely absent.

Show output
[unset]

Start from an empty environment

env -i env | wc -l

wc -l counts the lines that second env printed, and there are none: the environment it was handed was completely empty. -i throws away the inherited environment first and applies any NAME=value assignments on the same line afterwards, so it makes no difference whether you write -i before them or after.

Show output
0

Start from nothing plus what you name

env -i TIPS_A=1 TIPS_B=2 env

With -i the command's entire environment is what you wrote on the line, and nothing else. That is what makes this worth doing when a program behaves differently for two people, since it strips out every variable either of you might be carrying without knowing it.

Show output
TIPS_A=1
TIPS_B=2

A bare dash means the same as -i

env - TIPS_A=1 env

Older scripts use this spelling and env --help still documents it. Prefer -i as it's more explicit.

Show output
TIPS_A=1

printenv with no arguments

env -i TIPS_A=1 printenv

Given no names, printenv prints NAME=value pairs, the same listing env gives, without env's ability to change a variable or run a command. If you give it names it prints bare values instead, which is the form worth knowing: a script can read it without stripping a prefix off every line first.

Show output
TIPS_A=1

What a shell does when PATH is missing

env -i sh -c 'echo $PATH'

sh is dash on Debian, and it supplies a compiled-in default when it finds no PATH at all. So an empty environment does not mean an empty PATH, and a script that fails from cron is usually failing on a PATH that is short instead of absent.

Show output
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

bash's fallback PATH ends in the current directory

env -i bash -c 'echo $PATH'

The trailing . is bash's compiled-in default, reached only when nothing supplied a PATH. It means a file called ls in whatever directory the script happens to be sitting in, worth knowing before you run anything out of an unpacked archive this way.

Show output
/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/bin:/sbin:.

An empty PATH stops env finding the command

env -i PATH= sh -c 'echo hello'
echo "exit: $?"

env searches PATH for the command like any other program would, so the failure here is env reporting that it never got as far as sh. Give it /bin/sh and it runs.

Show output
env: 'sh': No such file or directory
exit: 127

Reproduce what a cron job is given

env -i /bin/sh -c 'echo PATH=$PATH; echo HOME=[${HOME-unset}]'

To diagnose a failing cron job, run the command this way and see whether it fails at your own prompt too. crontab can set both variables, and a PATH= line at the top of a crontab is usually the fix; works in the shell, fails in cron is that diagnosis end to end.

Show output
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOME=[unset]

PATH, and what env can run

env looks the command up on PATH the way the shell does, then replaces itself with it using execvp. The exit status says which part of that step failed.

A command that is not on PATH

command -v greet || echo "greet: not on PATH"

~/tools/greet is executable and runnable by full path. The shell will not find it by name, because ~/tools is not one of the directories in PATH. command not found triages the rest of the reasons for that message.

Show output
greet: not on PATH

Ask the child shell where it found the command

env PATH="$HOME/tools:$PATH" sh -c 'command -v greet'

command -v is a shell builtin, so there is no program of that name for env to execute; wrapping it in sh -c gives it a shell to run inside. The answer is the file the PATH change made findable.

Show output
/home/user/tools/greet

Give sudo a PATH it would otherwise ignore

sudo env PATH="$HOME/tools:$PATH" greet

sudo replaces your PATH with the secure_path compiled into it, so sudo greet fails however well greet works for you. $HOME is expanded by your own shell here, before sudo runs. Decide that you trust the directory first: this hands root a search path you control.

Show output
hello from greet

env cannot run a shell builtin

env cd /tmp
echo "exit: $?"

cd is part of the shell and there is no program of that name to exec. Aliases and shell functions are refused for the same reason.

Show output
env: 'cd': No such file or directory
exit: 127

127 when the command is not found

env nosuchcommand
echo "exit: $?"

The status a shell uses for the same failure, so a caller checking for 127 gets the same answer whether or not env was in the way.

Show output
env: 'nosuchcommand': No such file or directory
exit: 127

126 when it is found and cannot be run

env /etc/hostname
echo "exit: $?"

The file exists and is not executable. Splitting this from 127 lets a script differentiate a typo in a name from a missing execute bit.

Show output
env: '/etc/hostname': Permission denied
exit: 126

125 when env itself fails

env --nosuchoption true
echo "exit: $?"

125 is reserved for env's own failures, so they stay distinguishable from anything the command could have returned. A missing -C directory reports it too.

Show output
env: unrecognized option '--nosuchoption'
Try 'env --help' for more information.
exit: 125

Otherwise, the command's own exit status

env false
echo "exit: $?"

env replaces itself with the command instead of waiting on it, so there is no process left to substitute a status of its own. Exit codes covers what a caller does with this.

Show output
exit: 1

env on a shebang line

#!/usr/bin/env python3 is the reason most people have env in a file they wrote. The kernel runs the interpreter named on that line with the script as an argument, and naming env instead of the interpreter means the interpreter is looked up on PATH.

Find the interpreter on PATH

printf '#!/usr/bin/env bash\necho "$0 ran under $(command -v bash)"\n' > build
chmod +x build
./build

#!/usr/bin/bash would name one path and fail everywhere the interpreter lives somewhere else, which for anything installed by a version manager or a virtualenv is normal.

Show output
./build ran under /usr/bin/bash

Two words after env do not work

printf '#!/usr/bin/env bash -e\necho reached\n' > build
chmod +x build
./build
echo "exit: $?"

The kernel passes everything after the interpreter as one argument, so env is asked for a program called bash -e. The advice in the second line is env's own.

Show output
env: 'bash -e': No such file or directory
env: use -[v]S to pass options in shebang lines
exit: 127

Split that argument with -S

printf '#!/usr/bin/env -S bash -e\nfalse\necho reached\n' > build
chmod +x build
./build
echo "exit: $?"

-S splits the string it is given into separate arguments, which is the only reason it exists. -e is in force here, so the script stops at false and never reaches the echo.

Show output
exit: 1

Set a variable on the shebang line

printf '#!/usr/bin/env -S LC_ALL=C awk -f\nBEGIN { print "awk ran with LC_ALL=" ENVIRON["LC_ALL"] }\n' > summarise
chmod +x summarise
./summarise

Assignments work inside -S exactly as they do on a command line, so a script can pin its own locale without a wrapper. awk reads the result out of ENVIRON.

Show output
awk ran with LC_ALL=C

The interpreter is only found if PATH has it

printf '#!/usr/bin/env mylang\nthe body\n' > check
chmod +x check
./check
echo "exit: $?"

A shebang that searches costs you this much: the script runs under whatever mylang comes first on the caller's PATH, and fails outright when their PATH holds none. A cron job or a systemd unit is exactly where that shows up.

Show output
env: 'mylang': No such file or directory
exit: 127

The same script, with the interpreter on PATH

printf '#!/bin/sh\necho "mylang got: $1"\n' > "$HOME/tools/mylang"
chmod +x "$HOME/tools/mylang"
printf '#!/usr/bin/env mylang\nthe body\n' > check
chmod +x check
env PATH="$HOME/tools:$PATH" ./check

The interpreter receives the script's path as its first argument, which is how it knows what to read. The kernel decides from the shebang line alone and never from the body below it.

Show output
mylang got: ./check

Options that change how the command is started

Beyond the environment itself, env can set the working directory, the name the command sees itself by, and what it does with signals, all for that one command.

Run the command somewhere else

env -C /usr/share/doc pwd

-C changes directory before the exec, so the calling shell stays where it was. cd somewhere && command in a subshell is the portable spelling of this.

Show output
/usr/share/doc

Change the name the command sees itself by

env -a deploy-tool sh -c 'echo $0'

-a sets argument zero: the name a program prints in its own usage message, and the one ps reports. Busybox-style programs choose their behaviour from it.

Show output
deploy-tool

Watch what env decided

env --debug TIPS_TOKEN=secret id -un

Every step, in order, then the command's own output. Useful when a shebang line or a generated command is not doing what it reads as.

Show output
setenv:   TIPS_TOKEN=secret
executing: id
   arg[0]= 'id'
   arg[1]= '-un'
user

Start a command with a signal ignored

env --ignore-signal=INT sh -c 'kill -INT $$; echo still here'

A disposition rather than a handler, so it survives into anything the command starts. --block-signal delays delivery instead, and --default-signal puts one back for a command started from a shell that had ignored it.

Show output
still here