logo hsb.horse
← Back to blog index

Blog

Behavior of sh -c and rules for reading zsh configuration files

Organized the behavior of the sh -c command, the inheritance rules of environment variables, the validity of shebang, and how zsh configuration files (.zshrc, .zshenv, etc.) are read when scripts are executed.

Published:

We will organize the sh -c command and the behavior when calling a script from it, especially how the configuration files around zsh (.zshrc, etc.) are read.

This is the point where you can easily get hooked on Docker entry points, CI/CD settings, and calls from external tools.

What is sh -c?

In short, it “interprets the argument string as a command and executes it.”

Terminal window
sh -c "echo Hello World"

I’m talking about how it’s different from typing a command normally, but it’s mainly required in the following scenes.

When you want to redirect with sudo

sudo echo "text" > /etc/file results in a permission error. This is because the redirect is performed with general user privileges.

sudo sh -c "echo 'text' > /etc/file" succeeds because the entire shell runs as root.

When you want to use && or | with Docker/K8s

If you want to connect multiple commands or use a pipe with CMD or ENTRYPOINT, the syntax will not be parsed unless you go through the shell.

When you want to run Go binaries etc. via sh

sh ./my-binary causes an error because it “attempts to read the binary as a shell script.”

sh -c ./my-binary says “Execute the command in that path,” so it can be executed even in binary.

Environment variable inheritance rules

Are environment variables passed to the script called with sh -c?

verification code

Terminal window
# 親側
export MY_ENV="hoge"
sh -c "./sample.sh"
sample.sh
echo "$MY_ENV"

Results

  • Cover if exported (hoge appears)
  • Does not pass if not exported (will be empty)

The export variable is inherited from the child process (sh -c) to the grandchild process (sample.sh). Of course, you need to be aware that sh -c will launch a separate process.

Is Shebang effective?

When calling the script for zsh from main.sh via sh -c.

main.sh```sh sh -c ”./sample.sh”

**sample.sh**```sh
#!/usr/bin/env zsh
# zsh特有の処理...

Results

**It works properly as zsh. **

sh functions simply as a launcher, and the OS looks at the first line (Shebang) of sample.sh and starts zsh.

However, when reading with source (or .), it is NG.

  • sh -c "./sample.sh" → zsh starts and runs (Shebang enabled)
  • sh -c ". ./sample.sh" → Loaded as sh (Shebang ignored = syntax error)

Is the zsh configuration file read?

When sample.sh (zsh script) is executed, are .zshrc and .zprofile loaded?

Conclusion: Hardly loaded

File NameLoaded?Reason
.zshenvYESOnly read. Valid in all modes.
.zprofileNOIgnored because it is not a “login shell”.
.zshrcNO**Ignored because it is not an “interactive shell”. **
.zloginNOIgnored because it is not a “login shell”.

When running a script (non-interactive mode), .zshrc is skipped to avoid unnecessary output or aliases.

This is the reason why the alias written in .zshrc does not work in the script.

If you really want to load it

The only option is to explicitly source in the script.

#!/usr/bin/env zsh
source ~/.zshrc # 無理やり読み込む
my_alias_command

However, if .zshrc contains output such as echo, it will be mixed in with the script output, so this is not recommended.

The correct answer is to write the environment variables required by the script in .zshenv (regardless of whether it’s good or bad).

summary

sh -c is convenient, but it can get confusing if you don’t understand process boundaries and configuration file loading rules.

Especially in Docker and CI/CD environments, even if it works locally, it may not work inside the container. This is often caused by forgetting the environment variable export or depending on .zshrc.

It is safe to consolidate settings used in scripts into .zshenv and use functions and scripts instead of aliases.