Node.js と Bash で同じようにファイルパスを色々取得する

Node.js は process.cwd() とか path モジュールとかがあって、相対パスからフルパスを取得してみたり、拡張子だけ取得したりといったことが簡単かつ直感的にできる。

一方、Bash では不慣れなこともあって、どうやるんだっけーとなったので今回調べた。

目次

例とするファイル構成

例とするファイルとディレクトリの構成は次のようなモノを想定する。

/home/neo/parent/
└ child/
   ├ test.js
   └ test.bash

ターミナルのシェルのカレントディレクトリは /home/neo/parent/ にしておき、

$ node ./child/test.js
$ bash ./child/test.bash

という感じでそれぞれのスクリプトを実行し、結果を確認する。

それぞれのスクリプトでは、ディレクトリパスやファイルパスなどを色々と出力させるが、Node.js 版と Bash 版とで同じ結果を得ることを目標とする。

Node.js 版のスクリプト

組み込み変数の __filename__dirnameprocess の他、path モジュールを使っている。

const 定数に控えた結果を console.log() で出力しているが、同じ結果を得られる他のイディオムをコメントアウトで記載している。

#!/usr/bin/env node

const path = require('path');

// このファイルのフルパス
const filename = __filename;
console.log(`FILENAME : ${filename}`);
//console.log( __filename );
//console.log( process.argv[1] );

// このファイルがあるディレクトリまでのフルパス
const dirname = __dirname;
console.log(`DIRNAME  : ${dirname}`);
//console.log( __dirname );
//console.log( path.dirname(process.argv[1]) );
//console.log( path.parse(process.argv[1]).dir );

// このファイル名と拡張子
const basename = path.basename(process.argv[1]);
console.log(`BASENAME : ${basename}`);
//console.log( path.basename(process.argv[1]) );
//console.log( path.parse(process.argv[1]).base );

// このファイル名
const name = path.parse(process.argv[1]).name;
console.log(`NAME     : ${name}`);
//console.log( path.basename(process.argv[1], path.extname(process.argv[1])) );
//console.log( path.parse(process.argv[1]).name );

// このファイルの拡張子
const extname = path.extname(process.argv[1]);
console.log(`EXTNAME  : ${extname}`);
//console.log( path.extname(process.argv[1]) );
//console.log( path.parse(process.argv[1]).ext );

// このファイルを実行しているシェルのカレントディレクトリ
const cwd = process.cwd();
console.log(`CWD      : ${cwd}`);
//console.log( process.cwd() );
//console.log( path.resolve() );
//console.log( path.resolve('') );
//console.log( path.resolve('.') );

結果は次のとおり。

$ node ./child/test.js
FILENAME : /home/neo/parent/child/test.js
DIRNAME  : /home/neo/parent/child
BASENAME : test.js
NAME     : test
EXTNAME  : .js
CWD      : /home/neo/parent

Bash 版のスクリプト

dirnamebasename コマンドは大丈夫だと思う。拡張子をちぎるのに Bash の変数展開を利用している。他には sed を使ったりする方法もあるようだが今回は省略。

#!/bin/bash

# このファイルのフルパス
FILENAME="$(readlink -m "$0")"
echo "FILENAME : ${FILENAME}"
#echo "$(realpath -s "$0")"
#echo "$(readlink -f "$0")"

# このファイルがあるディレクトリまでのフルパス
DIRNAME="$(cd "$(dirname "$0")" ; pwd)"
echo "DIRNAME  : ${DIRNAME}"

# このファイル名と拡張子
BASENAME="$(basename "${FILENAME}")"
echo "BASENAME : ${BASENAME}"

# このファイル名
NAME="${BASENAME%.*}"
echo "NAME     : ${NAME}"

# このファイルの拡張子 (Node.js の `path.extname()` と揃えるため先頭にピリオドを付与している)
EXTNAME=".${BASENAME##*.}"
echo "EXTNAME  : ${EXTNAME}"

# このファイルを実行しているシェルのカレントディレクトリ
CWD="$(pwd)"
echo "CWD      : ${CWD}"

結果は次のとおり。

$ bash ./child/test.bash
FILENAME : /home/neo/parent/child/test.bash
DIRNAME  : /home/neo/parent/child
BASENAME : test.bash
NAME     : test
EXTNAME  : .bash
CWD      : /home/neo/parent

実行ファイルの拡張子が .js.bash で違うので、その違いだけ表れているが、Node.js 版と同じ結果が得られていることが分かるだろう。

存在しないディレクトリを挟んだフルパス構築に要注意

Node.js の path.resolve() では、存在しないディレクトリパスが挟まっても問題ない。単純に文字列として操作しているだけで、ファイルの存在有無は関係ないのだ。

const path = require('path');

const targetFilePath = path.resolve(__dirname, '../not-exist-dir/hoge/fuga/foo.txt');
// → '/home/neo/not-exist-dir/hoge/fuga/foo.txt'
// (`not-exist-dir/` ディレクトリが存在しなくても大丈夫)

Bash では、readlink -m を使えば存在しないディレクトリが挟まっても問題ないが、realpath -sreadlink -f の場合は不都合があるので注意。

#!/bin/bash

echo "$(readlink -m '../not-exist-dir/hoge/fuga/foo.txt')"
# → '/home/neo/not-exist-dir/hoge/fuga/foo.txt'
# (`not-exist-dir/` ディレクトリが存在しなくても大丈夫)

echo "$(realpath -s '../not-exist-dir/hoge/fuga/foo.txt')"
# realpath: ../not-exist-dir/hoge/fuga/foo.txt: そのようなファイルやディレクトリはありません
# (エラーになってしまう)

echo "$(readlink -f '../not-exist-dir/hoge/fuga/foo.txt')"
# 空文字になり何の結果も得られない

上述のスクリプトでは「スクリプトファイル自身のフルパス」を組み立てていたので、3つのコマンドのどれでも同じ結果が得られるが、例えば「ファイルのコピー先フルパス」を作ろうとして realpath -sreadlink -f を使った時に、ディレクトリが存在しない状態ではエラーになったりパスが空文字になったりすので注意、というワケだ。

process.argv[1]$0 の違い

ついでに、今回の検証で確認したこと。

Node.js でスクリプトファイル自身のパスを取得するのに process.argv[1] というイディオムを使っているが、コチラはシェル上で相対パス指定したとしても、必ずフルパスの文字列で取得される。逆にいうと、Node.js スクリプトの中からは

$ node ./child/test.js

というように「相対パスで記載した」のかどうか区別できない、ということになる。

一方、Bash の $0 は、

$ bash ./child/test.bash

のように相対パスで指定した場合は相対パスのままの文字列が受け取れる。ホームディレクトリを示す ~/ だけは /home/… と展開されるが、基本的にシェルで入力したとおりの文字列が取得できるのが $0 である。

以上

Bash で色々なパスを扱う際は、dirnamebasename コマンドの他、readlink -m あたりを覚えておくと良さそうだ。

参考文献

Node.js の話。

Bash の話。