env Command: Tutorial & Examples
Print the environment, or run a program inside a custom one — the little tool that shapes every process's world.
What It Is
env does two unrelated-looking things that are secretly the same thing. Run it with no arguments and it dumps the environment — the bag of NAME=VALUE settings every running program inherits. Give it a command instead, and it launches that command inside an environment you shaped: variables added, removed, or wiped clean. Same tool, one job seen from two sides: the environment is data you can read, and data you can rewrite before a program ever wakes up.
If you've never run a server, the "environment" is the piece that quietly explains a dozen mysteries: why a cron job can't find a command that works fine when you type it, why your app reads DATABASE_URL but your shell shows nothing, why a script behaves differently under sudo. All of those are environment problems, and env is the flashlight and the wrench. It ships in coreutils on every Linux box, it only changes things for the one command you ask it to, and by the end of this page you'll understand not just the flags but the invisible context every process is born into. We'll even meet the most famous line of code env ever wrote — the one sitting at the top of half the scripts on your machine.
Your First Look
Type the three letters, nothing else:
env
SHELL=/bin/bash
LANG=en_US.UTF-8
HOME=/home/arndt
PWD=/home/arndt/IdeaProjects/arndt/monitoring
USER=arndt
PATH=/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/bin:/sbin
TERM=xterm-256color
LOGNAME=arndt
SSH_AUTH_SOCK=/run/user/1000/openssh_agent
...
That's the environment of the current process — one NAME=VALUE per line, unsorted (they come out in whatever order they were set). Real screens are longer and messier than this tidy excerpt; a desktop session leaks dozens of XDG_*, GTK_*, and DBUS_* lines you'll learn to scroll past. The ones that matter on a server are a handful: PATH (where commands are found), HOME, USER/LOGNAME, SHELL, LANG, and PWD. Everything a program knows about who and where it is, before it's read a single config file, is right here.
Now the other half. Stick a command on the end and env runs it — but first it sets whatever NAME=VALUE pairs you wrote in front:
env GREETING=hello bash -c 'echo $GREETING'
hello
The variable GREETING existed for exactly the lifetime of that one bash, and vanished the instant it exited. Your own shell never had a GREETING and still doesn't. That ephemerality is the whole point, and it's what makes env safe to play with.
How I Use It
Over a shoulder, here's what env is actually for in daily server life — three jobs, in the order I reach for them.
First, plain env to see what a shell actually has. When something works in my terminal but breaks in a cron job or a systemd unit, my first move is to compare environments. The interactive shell I'm typing in has a fat, friendly environment built by my login files; the stripped-down context cron hands a job has almost nothing — frequently a PATH of just /usr/bin:/bin. Run env in both and the diff is the bug. (Nine times out of ten the answer is "cron's PATH doesn't include /usr/local/bin, where the binary lives.")
Second, env VAR=value command to inject a setting for one run without touching my shell or any config file. Test an app against a different database, flip on a debug flag, force a locale — one command, no cleanup, no risk of forgetting I'd exported something three hours ago and poisoning everything after.
Third — and this is the move worth the price of admission — env -i to run a command in a blank environment. Empty. No PATH, no HOME, nothing but what I explicitly hand it. This is how you reproduce exactly what cron or systemd sees, so you debug the real failure instead of the cozy one your interactive shell hides:
env -i FOO=bar bash -c 'echo "FOO=$FOO"; echo "PATH=$PATH"'
FOO=bar
PATH=/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/bin:/sbin:.
Notice PATH came back even though we wiped everything? That's bash itself supplying a fallback default when it starts with no PATH at all — a useful reminder that the shell sets up its own world too. Start the same way with a program that doesn't do that, and you'll watch it fail to find a single command. Which is, of course, precisely the lesson cron keeps trying to teach everyone.
The Flags, Explained
The whole flag set, including the obscure ones a veteran half-remembers:
-i,--ignore-environment— start from an empty environment instead of inheriting yours. The big one. A bare-means the same thing (env - command), a tiny piece of Unix shorthand that predates the long option.-u NAME,--unset=NAME— remove one variable before running the command. Repeatable:-u HOME -u LANG. Handy when you want almost your environment, minus one troublemaker.-0,--null— when printing the environment, end each line with a NUL byte instead of a newline. Why? Because a variable's value can legally contain a newline, so newline-delimited output is ambiguous; NUL can't appear inside a value, so it's the only safe separator for machines. Pair it withxargs -0and you can chew through the environment without a parsing bug.-C DIR,--chdir=DIR— change the working directory toDIRbefore launching the command.env -C /tmp pwdprints/tmp. Saves acd … &&dance, and it's exactly the kind of thing a service manager wants.-a ARG,--argv0=ARG— set the command's zeroth argument (the name it sees itself as). A genuine curio — a few programs change behavior based on the name they were called by (bashvsshis the classic), and this lets you lie to them about it.-S STR,--split-string=STR— split one string into several arguments. Looks pointless from the shell, but it's the hero of the#!/usr/bin/env -Sshebang trick below, where the kernel only hands the shebang one argument. It supports${VAR}expansion and nothing fancier — try$FOO(no braces) and it'll politely refuse.-v,--debug— narrate every step: what it unset, what it set, what it's about to execute. Your first call when anenvinvocation does something baffling.--block-signal[=SIG]/--default-signal[=SIG]/--ignore-signal[=SIG]— start the command with certain signals blocked, reset to default, or ignored. Niche, but real: it lets you launch a child that, say, ignores SIGINT without wrapping it in a script.--list-signal-handlingprints any non-default signal handling to stderr.
One rule that catches everyone: the first word after the options that isn't a NAME=VALUE pair is treated as the command, and everything after it as that command's arguments. So env FOO=1 BAR=2 myapp --verbose sets two variables and runs myapp --verbose. Order matters.
The Magic: The Shebang That Made Scripts Portable
Here's the bit that turns env from "handy" into "quietly everywhere." Look at the top of almost any Python, Ruby, or Node script written this century:
#!/usr/bin/env python3
That first line — the shebang — tells the kernel which interpreter to run the file with. The naive version hardcodes a path: #!/usr/bin/python3. And it works… until it doesn't. Because python3 lives in /usr/bin on Debian, /usr/local/bin on a from-source build, somewhere deep inside a virtualenv when one's active, and /opt/homebrew/bin on a Mac. Hardcode one path and your script breaks on every machine that put the interpreter somewhere else.
#!/usr/bin/env python3 sidesteps the whole problem with one move: the kernel runs env (which is reliably at /usr/bin/env, one of the few truly fixed locations in Unix), and env then looks up python3 on the current PATH and runs whichever one it finds first. You don't say where Python is; you say "use the Python this user would get if they typed python3." Portable by indirection. The script no longer cares about your filesystem layout — it borrows the same search PATH uses every time you type a command.
Pro Tip
The same indirection that makes it portable is exactly what makes a virtualenv "just work": activate the env, your
PATHgets the venv'sbinprepended, and#!/usr/bin/env python3now resolves to the venv's interpreter with zero changes to the script. It's not magic — it'sPATHresolution, deferred to runtime.
There's a famous wrinkle, and it's where -S rides in. The kernel passes everything after the interpreter name as a single argument, so #!/usr/bin/env python3 -O tries to find a program literally named "python3 -O" and fails. For years the fix was "you simply can't pass interpreter flags through env." Then GNU env grew -S, which splits that one argument back into many:
#!/usr/bin/env -S python3 -O
Now the kernel hands env the lump python3 -O, -S splits it, and you get flags through the portable shebang after all. A twenty-year papercut, healed with one letter.
Reading It by Example
Pattern recognition — readout, then verdict:
PATH=/usr/bin:/bin and your command "isn't found" in a cron or service context → the slim default PATH. Your binary is probably in /usr/local/bin. Either call it by full path or set PATH= at the top of the job. The single most common environment bug there is.
env shows no DATABASE_URL but the app insists it's set → the variable was exported in a different shell, or set inside a script that already exited, or it's in a .env file the app loads itself and never puts in the real environment. Variables don't travel up to a parent or sideways to a sibling — only down to children. (More on that one-way street under Gotchas.)
env -i myapp fails instantly with "command not found" or a locale warning → good, that's the truth surfacing. The program needs PATH/HOME/LANG that your interactive shell was silently providing. Add them back one at a time and you'll learn exactly which ones the program actually depends on.
env under sudo looks dramatically different from env as yourself → sudo deliberately scrubs most of the environment for safety (it keeps a small allow-list). That's a feature, not a bug, and the reason a script can behave one way for you and another under sudo.
How You'll Actually Use It
The handful that earn their keep:
env # what does this shell actually have?
env DEBUG=1 ./myapp # one variable, one run, no cleanup
env -i bash -c 'env' # what does a blank environment look like?
env -u LD_PRELOAD ./suspicious-binary # run it minus one variable
env -i PATH=/usr/bin VAR=x ./repro.sh # reproduce cron's stripped world exactly
That third one is a tiny revelation the first time: env -i bash -c 'env' shows you almost nothing, and suddenly you get why the same command behaves like a stranger inside cron. You're not debugging the program — you're debugging the world it was born into.
Gotchas
- Variables flow down, never up.
env FOO=1 bashthenecho $FOOinside that bash shows1; exit, and your original shell has noFOO. A child can never set a variable in its parent — which is the real reason "I set the variable but it's gone" happens, and why there's no command that "exports a variable to my current shell from a script." (The shellsource/.trick exists precisely to dodge this by not spawning a child.) envis not your shell.env FOO=barandFOO=bar somecmd(shell prefix syntax) usually do the same thing, butenvis a real program with no access to shell builtins —env cd /tmpfails becausecdisn't a program, it's a builtin. Useenv -C /tmp …instead.envvsprintenv. To read one variable,printenv HOMEis cleaner thanenv | grep HOME(which can match a value, not just a name).env's superpower is running things;printenv's is reading them.exportmakes a variable inheritable; plain assignment doesn't. A shell variable set withx=5is local to that shell andenvwon't show it;export x=5puts it in the environment that children — includingenv— actually see. The distinction trips up every beginner exactly once.env -Sonly expands${BRACED}form. Bare$FOOin a split-string is an error on purpose, to keep shebang lines predictable.
History & Philosophy
The environment is older and stranger than it looks. In the very first Unix there was no such thing — a program got its command-line arguments and nothing else. The environment was added in the mid-1970s as a second, out-of-band channel: a way to pass settings to a program without cluttering its argument list, inherited automatically by every child so you didn't have to re-thread it through every call. env the command arrived to give shell users a handle on that channel — read it, reshape it, hand a tailored copy to one program.
And here's the connecting thread worth pulling, BTW. The environment is just a flat array of strings the kernel copies into a new process at exec time — you can see a process's own copy any moment in /proc: cat /proc/self/environ prints it raw, NUL-separated (pipe it through tr '\0' '\n' to read it). Same data env shows you, straight from the source. So env is doing nothing privileged or magical; it's a thin convenience over a mechanism the kernel maintains for every process on the machine. That's the recurring Unix delight: the impressive-sounding thing (the environment) turns out to be a humble, inspectable file you can cat.
The deeper idea, the one that sticks: PATH resolution is late binding for executables. When you type a command, the shell doesn't know yet which file it'll run — it decides at the last instant by walking PATH. #!/usr/bin/env python3 simply borrows that same late binding for scripts, which is why "where is Python installed?" stopped being a question anyone has to answer. One small program, sitting at a fixed address, turning "the exact path" into "whatever the user would get" — and quietly making half the world's scripts portable in the process.
Cheat Sheet
env # print the current environment
env VAR=value cmd # run cmd with VAR added
env -i cmd # run cmd in an EMPTY environment
env -u VAR cmd # run cmd with VAR removed
env -C /dir cmd # run cmd in a different directory
env -0 # NUL-terminate output (for xargs -0)
env -v VAR=x cmd # debug: narrate every step
env -S 'prog -flag' # split one string into args (shebangs)
printenv VAR # cleanly read one variable's value
cat /proc/self/environ # the raw environment, kernel's own copy
Shebang lines worth memorizing:
#!/usr/bin/env python3 # portable: find python3 on PATH
#!/usr/bin/env -S python3 -O # portable AND pass interpreter flags
See Also
printenv— read one variable cleanly, no grepexport— mark a shell variable for inheritancexargs— the natural partner forenv -0sudo— and why it scrubs the environment- cron — the stripped-down environment that breaks everything
- systemd — sets each service's environment explicitly
- PATH — the search list that makes the shebang trick work
- shebang — the
#!line and how the kernel reads it - /proc — where a process's real environment lives
Ever lost an hour to a job that works in your terminal but dies in cron?
That gap is almost always the environment — a stripped
PATH, a missing variable, a service running in a world nothing like your shell's — and CleverUptime flags the symptoms in plain language when a service starts misbehaving, so you spend the hour fixing it, not finding it.Want to see your own server's health right now? One command, no signup, no install.