1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

source したのに export が効かない:Windows CRLF が仕掛けた環境変数の罠

1
Posted at

はじめに

Linux サーバ上でシェルスクリプトを運用していると、こんな経験をすることがある。

$ source env.txt
$ echo $FM_SRC_ROOT
/var/www/html        # ← 確かに見える
$ bash migration.sh
[NG] FM_SRC_ROOT が未設定です。先に env.txt を source してください

echo で見えているのに、子プロセスで消えている。

この症状、原因は source コマンドでも export でも bash でもなく、Windows の改行コード(CRLF) だった。
本記事では、CRLF が source をどう壊すかの仕組みと、nohup・cron・バックグラウンドジョブすべてで安全に動く実践パターンを紹介する。


症状の全体像

状況はこうだった。

  • 環境変数をまとめた設定ファイル env.txt を Windows の VSCode で編集していた
  • Linux サーバ上で source env.txt してからスクリプトを実行する運用だった
  • ある日、bash migration.sh を実行するとスクリプト内の変数チェックで弾かれた

エラー全文:

migration.sh: line 43: FM_SRC_ROOT: FM_SRC_ROOT が未設定です。
先に env.txt.prd または env.txt.stg を source してください

最初は「source し忘れ」を疑った。しかし source env.txt した直後でも再現する。
echo $FM_SRC_ROOT は値を返す。env | grep FM_SRC_ROOT にも出てくる。

なのに bash migration.sh の子プロセスでは消えている。


Part 1: 原因 — CRLF が bash を騙す仕組み

CRLF とは

テキストファイルの行末には「改行コード」が入っている。

OS 改行コード 表記
Linux / macOS LF のみ \n
Windows CR + LF \r\n

Windows の VSCode や メモ帳で保存したファイルは、デフォルトで各行の末尾に \r(CR、キャリッジリターン)が入る。Linux で通常どおりに表示すると \r は不可視だが、bash はちゃんと読んでいる。

bash が source するとき何が起きるか

source env.txt は env.txt の中身をそのまま現在のシェルで評価する。行末に \r がある場合、以下のような行が:

export FM_SRC_ROOT="/var/www/html"

bash には実際にはこう見える:

export FM_SRC_ROOT="/var/www/html"\r

結果として、FM_SRC_ROOT の値は /var/www/html ではなく /var/www/html + \r(CR 文字)になる。

なぜ echo では見えるのか

CR(\r)はターミナルに「カーソルを行頭に戻せ」と指示する制御文字だ。

$ echo $FM_SRC_ROOT
/var/www/html        # ← \r で行頭に戻ってから表示が終わる → 見た目は正常

値の末尾に \r があっても、echo の出力では CR がカーソルを行頭に戻すだけなので内容が表示される。値が存在しているように見えてしまう。

確認するには [ ] で囲む:

$ echo "[${FM_SRC_ROOT}]"
[/var/www/html        # ← \r で行頭に戻り ] が最初に表示される
]/var/www/html        # 実際にはこう見える(行頭に ] が上書きされる)

] が行頭に飛んでくる、あるいは表示が崩れたら CRLF 混入の証拠だ。

なぜ子プロセスで消えるのか

\r が混入した変数でも export 自体は成功することが多い。しかし問題は 使われるとき に起きる。

スクリプト内でよく使われるパターン:

: "${FM_SRC_ROOT:?FM_SRC_ROOT が未設定です}"

この :? 演算子は変数が「空文字列か未設定」のときにエラーを出す。値が /var/www/html\r の場合は空ではないので通過する。

しかし、次のようなパスチェックで失敗する:

[ -d "${FM_SRC_ROOT}" ]  # /var/www/html\r というパスは存在しない

また \r の混入した変数値を別変数と比較すると:

if [ "${FM_SRC_ROOT}" = "/var/www/html" ]; then  # 一致しない(\r が余分)

export そのものは通っているが、変数の値が壊れているため正常に使えない。

bash の実装やバージョンによっては、\r を含む変数名の export が無効になり、子プロセスへの継承自体が失敗するケースもある。いずれにせよ「echo で見える・子プロセスで使えない」という症状として現れる。


Part 2: 診断コマンド

Step 1: ファイルの CRLF を確認

cat -A env.txt | head -10

^M$ が行末に見えたら CRLF(^M が CR、$ が LF の位置)。

export FM_SRC_ROOT="/var/www/html"^M$    # CRLF あり ← これが問題
export FM_SRC_ROOT="/var/www/html"$      # LF のみ  ← 正常

file コマンドでも確認できる:

file env.txt
# CRLF → env.txt: ASCII text, with CRLF line terminators
# LF   → env.txt: ASCII text

Step 2: source 後に変数値を確認

source env.txt
printf '[%s]\n' "${FM_SRC_ROOT}"

正常なら [/var/www/html] と表示される。
CRLF 混入なら ] が行頭に飛ぶか表示が崩れる。

echo ではなく printf '%s\n' を使うと、\r を含む値でも改行前に内容が確認できる:

printf 'FM_SRC_ROOT=[%s]\n' "${FM_SRC_ROOT}" | cat -A
# FM_SRC_ROOT=[/var/www/html^M]$   ← ^M(CR)が見える

Step 3: 子プロセスへの継承を確認

source env.txt
bash -c 'printf "child: [%s]\n" "${FM_SRC_ROOT}" | cat -A'

親で \r 込みの値が export されていれば、子プロセスでも同じ壊れた値が見える。


Part 3: 修正 — sed をかませた source

基本パターン

# NG: 生 source(CRLF がそのまま混入)
source env.txt

# OK: \r を除去してから source
source <(sed 's/\r//' env.txt)

<(コマンド) は「プロセス置換」と呼ばれる bash の機能で、コマンドの出力を一時的なファイルディスクリプタとして扱う。sed 's/\r//' で全行の \r を除去してから source に渡すことで、CRLF の影響を受けない。

除去確認

source <(sed 's/\r//' env.txt)
printf 'FM_SRC_ROOT=[%s]\n' "${FM_SRC_ROOT}" | cat -A
# FM_SRC_ROOT=[/var/www/html]$   ← ^M が消えた

nohup・cron・バックグラウンドでも使える

source <(sed ...) は通常の source と同様にシェル組み込みコマンドなので、以下すべての呼び出し形式で有効だ:

# 対話シェルから source 後に実行
source <(sed 's/\r//' env.txt)
bash migration.sh

# nohup で長時間実行
source <(sed 's/\r//' env.txt)
nohup bash migration.sh > migration.log 2>&1 &

# cron(ただし外部 source には別の問題がある → Part 4 参照)

Part 4: 実践パターン — スクリプト内部で自動ロード

「外部 source 必須」運用の問題点

source env.txt && bash migration.sh という二段階の運用には落とし穴がある。

  1. cron で source が実行されない
    cron は非対話シェルで起動するため .bashrc.bash_profile を読まない。source env.txt を cron のコマンドに含めても、source 自体がシェル組み込みなので動作しないケースがある
  2. nohup から呼び出す wrapper スクリプトで継承が切れる
    親スクリプトが子スクリプトを bash child.sh で呼ぶと、export されていない変数は引き継がれない(export があっても \r 混入で実用上機能しないことがある)
  3. 手順が複雑でヒューマンエラーが起きやすい

解決策:スクリプト内部に env ロードを組み込む

スクリプトの冒頭で「必要な変数が未設定のときだけ env ファイルを探してロードする」パターンに変える。nohup・cron・バックグラウンドジョブ・直接実行、どの呼び出し方でも動く。

#!/bin/bash
# =============================================================
# 環境変数自動ロード
#   FM_SRC_ROOT が未設定の場合のみ実行。
#   source <(sed 's/\r//') で Windows CRLF も同時に除去する。
# =============================================================
if [ -z "${FM_SRC_ROOT:-}" ]; then
  _SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
  _ENV_LOADED=0

  # 引数で env ファイルを明示指定した場合
  if [ -n "$1" ]; then
    _env_file="$1"
    # 相対パスは SCRIPT_DIR 基準に変換
    [[ "${_env_file}" != /* ]] && _env_file="${_SCRIPT_DIR}/${_env_file}"
    if [ -f "${_env_file}" ]; then
      source <(sed 's/\r//' "${_env_file}")
      _ENV_LOADED=1
    else
      echo "[NG] 環境変数ファイルが見つかりません: ${_env_file}" >&2
      exit 1
    fi
  else
    # 引数なし → スクリプトと同じディレクトリから自動検出(prd → stg の順)
    for _suffix in prd stg; do
      _env_file="${_SCRIPT_DIR}/env.txt.${_suffix}"
      if [ -f "${_env_file}" ]; then
        source <(sed 's/\r//' "${_env_file}")
        _ENV_LOADED=1
        break
      fi
    done
  fi

  if [ "${_ENV_LOADED}" -eq 0 ]; then
    echo "[NG] 環境変数ファイルが見つかりません" >&2
    echo "  env.txt.prd または env.txt.stg を配置してください" >&2
    exit 1
  fi
fi

# 変数チェック(ここまで来れば必ず設定済み)
: "${FM_SRC_ROOT:?FM_SRC_ROOT が未設定です}"

ポイント解説

[ -z "${FM_SRC_ROOT:-}" ] で二重ロードを防ぐ

このガードにより、親スクリプトが既に source 済みの場合は env ファイルの読み込みをスキップする。親が export した値をそのまま引き継ぐ。

BASH_SOURCE[0] でスクリプトの実際の場所を取得

$0 では nohupsource 経由での呼び出し時にパスが変わることがある。BASH_SOURCE[0] を使うとスクリプトの実ファイルパスが取れる。フォールバックとして :-$0 も指定している。

source <(sed 's/\r//') で CRLF を除去しつつロード

ファイルを直接 source するのではなく、sed でフィルタリングしてから渡す。Windows で編集された env ファイルを LF に変換せずそのまま使える。

④ 引数指定と自動検出の両立

# 引数指定(明示的・推奨)
bash migration.sh env.txt.prd

# 引数なし(スクリプトと同じディレクトリの env ファイルを自動検出)
bash migration.sh
nohup bash migration.sh > migration.log 2>&1 &

# cron
0 3 * * * cd /path/to/script && bash migration.sh >> /var/log/migration.log 2>&1

どの形式でも env ファイルをロードして実行できる。


Part 5: cron での認証エラーも同じ根本原因

cron 経由でスクリプトを実行すると、gcloud コマンドが command not found になるケースがある。これも CRLF とは別の「非対話シェルでの PATH 継承切れ」問題だが、構造は同じだ。

症状

/bin/bash: gcloud: command not found
exit=127

cron のシェルは非対話モードで起動するため、.bashrc.bash_profile が読み込まれない。これらのファイルに書かれた PATH 設定(例:Cloud SDK のパス追加)が有効にならない。

解決策:env ファイルに PATH を明示

env ファイルに PATH を書いておき、スクリプト内部の自動ロードで引き込む:

# env.txt.prd に追加
export PATH="/opt/google-cloud-sdk/bin:${PATH}"
# スクリプト内部の自動ロード(CRLF 除去込み)で PATH も一緒にロードされる
source <(sed 's/\r//' "${_env_file}")

env ファイルに export PATH=... を書いておくことで、gcloud の PATH 設定も cron・nohup・バックグラウンドジョブすべてで有効になる。

サービスアカウント認証も env ファイルで管理

対話的な gcloud auth login は cron から使えない。サービスアカウントキーを使う場合:

# env.txt.prd に追加
export GOOGLE_APPLICATION_CREDENTIALS="/path/to/service-account-key.json"

これも source <(sed 's/\r//') でロードすれば cron から問題なく動く。


Part 6: まとめ

症状・原因・対処の対応表

症状 原因 対処
echo $VAR で見える・子プロセスで消える CRLF で \r が値末尾に混入 source <(sed 's/\r//' file)
パスが存在しない・条件分岐がおかしい 変数値に \r が付いている 上に同じ
source && bash script でも変数未設定 \r で export が実質無効 上に同じ
cron から実行すると変数が全部未設定 非対話シェルは .bashrc を読まない env ファイルをスクリプト内でロード
cron から gcloud: not found PATH が非対話シェルに引き継がれない env ファイルに export PATH=...

CRLF を除去する他の手段

方法 コマンド 用途
source 時に除去 source <(sed 's/\r//' file) ファイルを変換せず使う
ファイル自体を変換 sed -i 's/\r//' file Linux 側で一度だけ変換
dos2unix コマンド dos2unix file 専用ツール(要インストール)
エディタ設定 VSCode: 右下の改行コードを LF に変更 Windows 側で根本解決

ベストプラクティス

  1. env ファイルの変換は行わず source <(sed ...) で対応する
    ファイル自体を変換すると、次回 Windows で上書きしたときにまた CRLF に戻る。スクリプト側で吸収するほうが継続的に安全。

  2. スクリプトに env ロードを内包する
    source + bash script.sh の二段階運用は手順が増えてヒューマンエラーを招く。スクリプト自身に自動ロードを持たせれば呼び出し方を問わない。

  3. cat -A で CRLF を定期確認する
    Windows と Linux の共同作業では CRLF 混入がいつ起きてもおかしくない。CI で file *.sh | grep CRLF を走らせると早期発見できる。


おわりに

echo で見えているのに子プロセスで消える——この症状は、原因を知らないと何時間でも迷子になる。\r という 1 バイトが source の動作を壊し、無害に見える echo の出力が診断の邪魔をする。

最終的にたどり着いたのは source <(sed 's/\r//') という一行の改修だったが、それ以上に重要だったのは「スクリプト自身が env ファイルをロードする」設計への転換だった。nohup・cron・バックグラウンドジョブ、どこから呼ばれても同じように動く——そのための投資としては安い変更だった。

Windows と Linux を行き来する環境で似た症状に遭遇したとき、この記事が最初の手がかりになれば幸いだ。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?