Quick take: printenv lists environment variables — either all of them or a specific one. env lists variables and can also run a command in a modified environment: env VAR=value command. Use env -i to start with a completely empty environment. Use #!/usr/bin/env python3 in scripts to find the interpreter via PATH.

Introduction

Every process on a Linux system inherits a set of key-value pairs called environment variables from its parent. These control fundamental behaviour: which directory PATH searches for executables, where the home directory is, what language settings to use, whether a program is running in debug mode, and what database URL a web application connects to. Understanding how to inspect and manipulate this environment is a foundational Linux skill.

env and printenv are the two main tools for working with the environment from the command line. They look similar but have different strengths. printenv is a pure read tool — it shows you what is set. env is also a run tool — it can set variables and immediately run a command with those variables active, without permanently changing anything in the current shell session. That last capability is what makes env genuinely powerful: you can test how a program behaves under different environment configurations without touching your shell's state.

This guide covers both commands in depth: what they display, how to modify the environment for a single command, how to start with a completely clean environment for reproducible testing, and how environment variables flow through scripts, Docker containers, and systemd services. If you work with DevOps infrastructure, application deployment, or shell scripting, you will use these commands regularly.

Syntax

The basic syntax of both commands:

env [OPTIONS] [NAME=VALUE ...] [COMMAND [ARG ...]]
printenv [OPTION] [VARIABLE ...]

When called with no arguments, both commands print the full environment. The key difference is that env can optionally run a command at the end, while printenv cannot.

What Are Environment Variables

An environment variable is a named value that the operating system passes to every process when it starts. The variable lives in the process's memory, not in a file — when the process exits, the variable disappears. Child processes (programs launched by your shell) receive a copy of the parent's environment, which they can modify for their own children but cannot push back up to the parent.

The most important variables you will encounter regularly are: PATH (colon-separated list of directories the shell searches for commands), HOME (your home directory), USER and LOGNAME (your username), SHELL (path to the current shell binary), LANG and LC_ALL (locale settings that control language and character encoding), TERM (the terminal type, which affects colour output and key bindings), PWD (the current working directory), and TMPDIR or TMP (the temporary file directory).

Applications add their own variables on top of these. A Django web application reads DATABASE_URL, SECRET_KEY, and DEBUG. A Node application reads NODE_ENV and PORT. AWS CLI tools read AWS_PROFILE, AWS_REGION, and AWS_ACCESS_KEY_ID. Docker reads DOCKER_HOST. These application-level variables are what you most often need to set, inspect, and override in a DevOps workflow.

Viewing Environment Variables

To print all environment variables currently set in your shell:

# Print every environment variable (both work)
env
printenv

# Print a single variable
printenv HOME
printenv PATH

# Print multiple specific variables
printenv HOME USER SHELL

# Check whether a variable is set (exits 0 if set, 1 if not)
printenv MY_VAR > /dev/null && echo "set" || echo "not set"

The output of env and printenv with no arguments is essentially the same: a list of NAME=value pairs, one per line. On a typical Ubuntu system you will see 30 to 60 variables. The output is not sorted by default, so pipe it through sort if you want alphabetical order:

printenv | sort
env | sort | grep -i path

To search for variables matching a pattern, combine with grep:

# All variables containing "JAVA"
printenv | grep JAVA

# All variables containing "AWS"
env | grep AWS

# Check what Python-related variables are set
printenv | grep -i python

Setting Variables for a Single Command

This is the most useful feature of env. You can set one or more variables and immediately run a command with those variables active. The variables exist only for that one command and do not affect the current shell or any future commands:

# Run a Python script with a different DATABASE_URL
env DATABASE_URL=postgres://testdb:5432/mydb python manage.py migrate

# Start a Node server on a different port without changing your shell
env PORT=9000 NODE_ENV=production node server.js

# Run a script with AWS credentials for a different account
env AWS_PROFILE=staging aws s3 ls

# Override PATH for one command to use a specific binary version
env PATH=/opt/python3.12/bin:$PATH python --version

The shell actually supports this syntax directly — you can write DATABASE_URL=value command without the env keyword and get the same result. But writing env explicitly makes the intent clear in scripts, especially when setting multiple variables. Both forms are equivalent:

# These two are identical
DATABASE_URL=postgres://localhost/mydb python app.py
env DATABASE_URL=postgres://localhost/mydb python app.py

Where env becomes indispensable is when you want to override an existing variable. If your shell already has DATABASE_URL set to production, env DATABASE_URL=test python app.py passes the test value to the command while leaving your shell's production value intact. The current shell is not modified at all.

Running in a Clean Environment

The -i flag starts the command with a completely empty environment — nothing inherited, no PATH, no HOME, no TERM, nothing:

# Run bash with zero inherited variables
env -i bash --norc --noprofile

# Run a program and see exactly what environment it sees
env -i HOME=/tmp PATH=/usr/bin:/bin MY_VAR=hello bash -c 'env'

# Test a Python script with only explicitly passed variables
env -i PYTHONPATH=/app DATABASE_URL=sqlite:///test.db python app.py

The clean environment is useful for testing reproducibility. If your application behaves differently on your laptop than on the CI server, one cause is differing environment variables. Running your test suite under env -i with only the variables your application explicitly documents reveals whether it silently depends on something from your personal shell configuration.

It is also the right approach for security-conscious script execution. When running a third-party script that you do not fully trust, giving it an empty environment prevents it from reading values like AWS_ACCESS_KEY_ID, GITHUB_TOKEN, or DATABASE_URL that may be set in your current session.

Common Options and Parameters

OptionDescription
env (no args)Print all environment variables for the current process.
env NAME=VALUE cmdRun cmd with NAME set to VALUE (inherits current environment plus override).
env -i NAME=VALUE cmdRun cmd with a completely clean environment containing only explicitly listed variables.
env -u NAME cmdRun cmd with the variable NAME removed from the environment.
env -0Separate output lines with null bytes instead of newlines — safe for filenames with spaces.
printenv (no args)Print all environment variables, one per line.
printenv NAMEPrint the value of NAME only. Exits 1 if the variable is not set.
printenv -0Separate output with null bytes instead of newlines.

Practical Examples

Real-world uses from daily infrastructure work:

# Inspect PATH in a readable format (one directory per line)
printenv PATH | tr ':' '\n'

# Check if a required variable is set before running a script
if ! printenv DATABASE_URL > /dev/null 2>&1; then
  echo "ERROR: DATABASE_URL is not set" >&2
  exit 1
fi

# Run Docker build with custom build args from environment
env | grep '^BUILD_' | while read -r line; do
  key="${line%%=*}"
  val="${line#*=}"
  echo "--build-arg $key=$val"
done

# Compare environments between two users
sudo -u appuser printenv | sort > /tmp/appuser.env
printenv | sort > /tmp/myenv.env
diff /tmp/myenv.env /tmp/appuser.env

# Remove a variable for one command (env -u)
env -u http_proxy curl https://internal-api.company.com/health

# Run a cron-like environment test (cron has minimal PATH)
env -i PATH=/usr/bin:/bin HOME=/root /path/to/script.sh

The diff between two users' environments is a technique I use regularly when a script works for me but fails for the service account. Nine times out of ten, the culprit is a missing variable — a JAVA_HOME, a proxy setting, or a custom PATH entry that exists in my interactive session but was never added to the service account's profile.

The env -u flag is less commonly known but very useful. If you have an HTTP proxy set in your environment and need to make one direct connection that should bypass it, env -u http_proxy -u https_proxy curl https://target removes both proxy variables for just that one command without unsetting them in your shell.

env and printenv in Shell Scripts

In shell scripts, printenv is the correct tool for checking and reading environment variables because it reflects the actual environment the script inherited — not just shell variables that might be set with VAR=value without export:

#!/bin/bash
# Validate required environment at script start
REQUIRED_VARS=(DATABASE_URL SECRET_KEY REDIS_URL)

for var in "${REQUIRED_VARS[@]}"; do
  if ! printenv "$var" > /dev/null 2>&1; then
    echo "ERROR: Required environment variable $var is not set."
    exit 1
  fi
done

echo "All required variables are present. Starting..."

This pattern — validating required variables at the top of a script before doing any real work — is a practice I enforce on every deployment script. It is far better to fail immediately with a clear error message than to proceed and fail cryptically halfway through a database migration because DATABASE_URL was never set.

When you need to pass environment variables to a subprocess in a script, env gives you control over exactly what the child process receives:

#!/bin/bash
# Run the test suite in a clean environment with only what it needs
env -i \
  PATH=/usr/local/bin:/usr/bin:/bin \
  HOME=/tmp \
  DATABASE_URL="sqlite:///test.db" \
  SECRET_KEY="test-secret-not-for-production" \
  python -m pytest tests/

env vs export vs set

These three commands all deal with variables but at different scopes and for different purposes:

set — a shell built-in that shows all shell variables, including ones that have not been exported to the environment. Local variables defined inside scripts with x=1 appear in set output but not in env or printenv output. set is entirely internal to the current shell process.

export — a shell built-in that marks a shell variable as part of the environment, so it is inherited by child processes. Without export, a variable set with VAR=value is local to the shell and invisible to any commands you run. Running export VAR=value is what permanently adds it to the environment for the duration of the shell session.

env — an external command (not a shell built-in) that reads the process environment, not the shell's internal variable table. This distinction matters: if you define a variable without export, env will not see it. env can also run a command with a modified environment, which neither set nor export can do.

# Unexported variable — visible in set, not in env
MY_LOCAL=hello
set | grep MY_LOCAL       # shows MY_LOCAL=hello
env | grep MY_LOCAL       # shows nothing
printenv MY_LOCAL         # shows nothing (exits 1)

# Exported variable — visible everywhere
export MY_GLOBAL=world
set | grep MY_GLOBAL      # shows MY_GLOBAL=world
env | grep MY_GLOBAL      # shows MY_GLOBAL=world
printenv MY_GLOBAL        # prints: world

env in Script Shebangs

One of the most important uses of env is at the top of scripts. The shebang line (#!) on the first line of a script tells the kernel which interpreter to use. If you hardcode the path like #!/usr/bin/python3, the script breaks on systems where Python is installed elsewhere — a common situation with virtual environments, Homebrew on macOS, or Python version managers like pyenv.

The portable solution is to use env to find the interpreter via PATH:

#!/usr/bin/env python3
#!/usr/bin/env bash
#!/usr/bin/env node
#!/usr/bin/env perl

/usr/bin/env is almost universally present in that exact location across all Unix-like systems, making it a reliable anchor point. When the kernel runs the script, it executes /usr/bin/env python3, which searches PATH and runs the first python3 it finds — which might be your virtual environment's Python, a pyenv shim, or the system Python, depending on what PATH is set to at runtime.

This pattern is particularly important for scripts that need to work inside Python virtual environments or Node version managers. If you activate a virtualenv before running a script with #!/usr/bin/env python3, the script automatically uses the virtualenv's Python because virtualenv prepends its bin directory to PATH.

Common Mistakes

The most common mistake is setting a variable without export and wondering why child processes cannot see it. If you write DATABASE_URL=postgres://localhost/mydb in a shell session and then run your application, the application will not see that variable. You need either export DATABASE_URL=postgres://localhost/mydb to make it persistent in the session, or DATABASE_URL=postgres://localhost/mydb ./myapp to pass it for just that one invocation.

A second common mistake is confusing shell variable syntax with environment variable syntax. In bash, $VAR is expanded by the shell before the command runs. So env VAR=$OTHER_VAR command works fine — bash expands $OTHER_VAR first, then passes the result to env. But if you want to pass the literal string $OTHER_VAR to the command, you need single quotes: env VAR='$OTHER_VAR' command.

Another issue I see regularly in CI pipelines: environment variables with newlines or spaces in their values. These need careful quoting. When you export a multi-line value like an SSH key or a certificate, make sure to quote it properly in every context where it is used — unquoted values with whitespace get silently truncated or split into multiple arguments.

Tips and Best Practices

  • Use printenv VAR rather than echo $VAR to read environment variables in scripts — it avoids shell expansion issues and correctly reports an unset variable by exiting with code 1.
  • Always validate required environment variables at the top of deployment scripts before doing any real work, so failures are immediate and clear rather than mysterious mid-execution errors.
  • Use #!/usr/bin/env interpreter shebangs in scripts for portability across systems with different installation paths.
  • Prefer env -i for running test suites to ensure tests are not silently passing because of variables from your personal shell configuration.
  • In Docker files, declare environment variables explicitly with ENV instructions rather than relying on the build environment — what is in your shell during docker build does not automatically enter the container.
  • Never put secrets directly in environment variable listings in logs or debug output. If your deployment script runs printenv and logs the output, it will expose every secret in your environment to whoever has log access.
  • Use env -u VARIABLE command to unset a specific variable for one command rather than unsetting it in the shell, which could break other things running in the same session.

Final Thoughts

Environment variables are the primary interface between the operating system, the shell, and running applications. printenv gives you a clear view of what is set, and env gives you a precise way to control what a specific command sees — without touching your shell's state at all. These two capabilities together make environment debugging straightforward: you can see exactly what the process will inherit before it runs, and you can modify it for a single invocation to test hypotheses.

The techniques that matter most in practice are: validating required variables in scripts, using env VAR=value command to override for one run, using env -i for clean reproducible tests, and using #!/usr/bin/env shebangs for portable scripts. Get comfortable with these patterns and you will spend far less time debugging "it works on my machine" environment problems.

One underrated use of env in DevOps pipelines is confirming what environment a containerised process actually receives at runtime. When a Docker container or Kubernetes pod behaves differently than expected, running env as the container entrypoint (temporarily) prints the full environment the container started with — exposing missing secrets, wrong variable names, or unexpected overrides from ConfigMaps and Secrets that were not set the way you expected. It is a five-second diagnostic that has saved me hours of guesswork on misconfigured deployments.

FAQ: env and printenv Commands in Linux

What is the difference between env and printenv?+

printenv prints environment variables and is read-only. env can also set variables for a single command invocation and run that command in a modified environment. For simply listing variables, both work; for setting a variable temporarily for one command, use env.

How do I set an environment variable for just one command?+

Prefix the command with VAR=value, for example: DATABASE_URL=postgres://localhost/mydb python app.py. env is implied — the shell handles this syntax directly. You can also write it as env VAR=value command for clarity.

What is the difference between env -i and a normal env call?+

env -i starts the command with a completely empty environment — no PATH, no HOME, nothing inherited from the current shell. This is useful for testing how a program behaves without any inherited configuration.

How do I print a single environment variable?+

Use printenv VAR — for example printenv HOME prints your home directory. You can also use echo $HOME but printenv avoids any shell variable substitution and prints the actual environment value.

How do I make an environment variable permanent?+

Add export VAR=value to your ~/.bashrc (for interactive shells) or ~/.profile (for login shells). For system-wide variables, add them to /etc/environment or a file in /etc/profile.d/. Run source ~/.bashrc to apply without logging out.

Need help with Linux servers or infrastructure?

Work directly with Muhammad Irfan Aslam for Linux, Ubuntu, Docker, DevOps, cloud, CI/CD, or infrastructure support.

Hire Me for Support