kmail.at
contact

elsewhere

EnglishEN
← learning

shell · difficulty ◆

env — set, strip, and run commands in a custom environment

Run one command with a different world around it — without touching yours.

You use it every time any script runs, and you've probably never typed it once — until today.

AI-generated draft · reviewed and editedThis text was generated with AI as a first draft, then reviewed and edited by the author, who holds editorial responsibility for it.

2026-09-16 · 4 min read

$ env

What it does

env runs a child command with an environment you control, then walks away — your shell's variables stay untouched. Prefix the command with NAME=VALUE pairs to add variables for that one child only, use -u NAME to yank one out, or -i to wipe the slate completely and start from nothing. No command? Plain `env` prints the whole environment, and `env | sort` gives you the readable version. It's the smallest, most portable way to hand a child a different reality for its lifetime.

Why it matters

Most tools read their configuration from environment variables: database URLs, API keys, locales, PYTHONPATH, debug flags. env lets you change exactly one of those for exactly one invocation instead of `export`-ing and un-exporting around it. When a cron job fails silently because PATH is wrong or LANG is C, `env -i` becomes an instant lab: it shows you exactly what a cold, minimal environment does to your command. It's the difference between guessing and reproducing.

Where the exit status comes from

env's exit status is mostly not its own. When it launched a utility, env passes that utility's status straight through — the reason a wrapper never hides a failure. When it could not launch one, POSIX fixes three codes: 126 if the utility was found but could not be invoked, 127 if it could not be found, and 1–125 for env's own errors. On this box `env nosuchutil` prints a 'No such file or directory' diagnostic and exits 127. That matters in scripts: a shebang of `#!/usr/bin/env python3` on a machine without python3 fails this way, and `$?` tells you 127 rather than something ambiguous.

Example

$ env | sort | grep -E '^(HOME|LANG|LOGNAME|SHELL|TERM|USER)='
HOME=/home/kmail
LANG=en_US.UTF-8
LOGNAME=kmail
SHELL=/bin/bash
TERM=xterm-256color
USER=kmail

With no command argument, env prints every variable — 118 of them on this box, which is why you sort and filter to the ones you wanted. It is the same dump printenv produces without the sorting; reach for printenv NAME when you only need one value.

$ env DB_HOST=db.internal APP_PORT=5433 printenv DB_HOST APP_PORT
db.internal
5433

The classic use: add variables for one child only. printenv sees them, but as soon as this line finishes they're gone — nothing leaks back into your shell. This is how you hand a Python or Node one-shot a database URL without exporting it.

$ export FIZZ=buzz
env -u FIZZ bash -c 'echo "inside  env -u FIZZ: FIZZ=${FIZZ-unset}"'
echo "after:   FIZZ=${FIZZ-unset}"
inside  env -u FIZZ: FIZZ=unset
after:   FIZZ=buzz

-u removes a variable for the child only. FIZZ is exported in this shell, stripped inside the env call, and still set here afterwards — a surgical strip for one command, with no cleanup step. Drop the export and you get the same two lines, because an unexported shell variable never reaches a child in the first place. That is also the check that explains most 'it works in my shell but not under cron' bugs.

$ env -i bash -c 'echo "HOME=${HOME-unset} PATH=$PATH"'
HOME=unset PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

-i wipes the entire environment and gives the child only what the program itself sets up — here bash rebuilt a minimal PATH from default compilation settings. This is the first tool for debugging cron jobs that work by hand and die on the schedule.

$ env PYTHONUNBUFFERED=1 python3 -c 'import os; print(os.environ.get("PYTHONUNBUFFERED"))'
1

Inject one tuning variable into a child with zero pollution: the parent never sees PYTHONUNBUFFERED after the line ends. Handy for forcing line-buffered output or flipping a library flag without editing any config file.

$ env LC_ALL=C.UTF-8 LANG=C.UTF-8 python3 -c 'import locale; print(locale.getlocale())'
('C', 'UTF-8')

Force a sane locale for one child that misdetects it — a classic fix for scripts that crash on non-ASCII output or sort differently depending on who launched them. Same pattern fixes date and sort behavior in tight cron environments.

Common flags

NAME=VALUE COMMAND
Set variables for the child only, then run COMMAND — the whole point of env; nothing is exported to your shell.
-u, --unset=NAME
Remove NAME from the child's environment. Multiple -u flags stack: env -u http_proxy -u https_proxy curl ...
-i, --ignore-environment
Start the child with an empty environment. Great for reproducing minimal/cron environments; beware it also drops PATH unless you re-add it.
-C, --chdir=DIR
Change the working directory before running the command (GNU coreutils ≥ 8.28, released 2017-09-01; FreeBSD gained its own -C in 14.2). Replaces the cd ... && env pattern.
printenv
Not a flag, but env's sibling: printenv NAME prints a single variable value, and printenv with no args matches env with no command. Use it to read one value without scrolling a wall of text.
-0, --null
Terminate output lines with NUL instead of newline — safe to pipe into xargs -0 when a value contains newlines.
env --help
Quick reference for every option, including the more exotic -S string-splitting and --default-signal handlers on newer coreutils.

History

From System V to every script on the box

env is an old, single-purpose utility — the BSD manuals date their implementation of it to 4.4BSD — and its core was standardised early: POSIX.2 specified env with -i and NAME=VALUE pairs, which is why the basic forms behave the same on macOS, Linux and every BSD. Everything else is an extension: -u, -0, -C, -S and the signal options all come from GNU or BSD, and FreeBSD's own manual is careful to list them as non-standard. That narrow standard core is what lets you copy a one-liner across three operating systems and trust it — but check -u before you rely on it somewhere unusual.

The shebang that launched a thousand tools

The most famous use is the interpreter lookup: #!/usr/bin/env python3. Instead of hardcoding one absolute path (e.g. /usr/local/bin/python3), env finds 'python3' by walking PATH — so the same script works under pyenv, virtualenv, conda, and the system install alike. This pattern is why version managers and every virtualenv-generated script work at all. env isn't just a utility here; it's the runtime resolver for a huge fraction of the ecosystem.

Fun facts

Pros & cons

pros

  • + Surgical, non-polluting: add or strip variables for one command and your shell stays clean
  • + POSIX-standard, identical behavior across every Unix — ideal for copy-paste portability
  • + The foundation of #!/usr/bin/env shebangs, making scripts run under every language manager

cons

  • − Doesn't persist anything — if you need the variable to survive, you must export it yourself
  • − Inherits variables wholesale from the parent, so an `env | sort` dump can leak sensitive values if you're careless
  • − Confusingly thin docs for a utility that quietly does a lot (chdir, signal handling, null output)

Takeaways

  1. 1Prefix any command with NAME=VALUE to inject variables for that one child call — no export, no cleanup
  2. 2env -u NAME removes a variable for one child; stack multiple -u flags to strip several at once
  3. 3env -i reproduces a cron/minimal environment — the fastest way to reproduce a script that breaks on the scheduler
  4. 4No command argument means 'print the environment' — pipe through sort for a readable dump, or use printenv NAME for one value
  5. 5Reach for #!/usr/bin/env python3 over a hardcoded absolute path so every language manager works

Related commands