1. 概要

シェルスクリプトを記述したり、Linuxコマンドラインを操作したりする場合、ファイルパス文字列を処理する必要があることがよくあります。 指定されたパス文字列から最後のディレクトリまたはファイル名を抽出することは、かなり一般的な操作です。

たとえば、特定のパス文字列「 / tmp / dir / target 」に対して、結果として「target」を取得しようとします。

はい、これは非常に単純な問題のように見えます。 上記の例を読んだときに、おそらくいくつかの解決策がすでに頭に浮かぶかもしれません。 ただし、単純な問題には、ソリューションを壊すいくつかのコーナーケースが含まれる場合があります。

このチュートリアルでは、この問題を詳しく調べ、一般的な解決策を評価します。

2. 一般的なソリューションの議論

Linuxファイルシステムでは、スラッシュ(/)をファイル名またはディレクトリ名の一部にすることはできません。

したがって、入力パス文字列をスラッシュで区切られた値と見なすと、最後の値を取得して問題を解決できます。

Linuxコマンドの武器を見ると、 grep sed awk など、多くの強力な武器がその仕事を支援する可能性があります。

$ sed 's#.*/##' <<< "/tmp/dir/target"
target

$ awk -F'/' '{print $NF}' <<< "/tmp/dir/target"             
target

$ grep -o '[^/]*$' <<< "/tmp/dir/target" 
target

または、Bashのパラメーター置換を使用して、問題を解決することもできます。

$ INPUT="/tmp/dir/target"
$ echo ${INPUT##*/}                
target

もちろん、他のコマンドラインツールを使用した同様のソリューションがもっとたくさんある可能性があります。 しかし、それらは問題に対する本当に安定した解決策ですか?

Linuxでは、ディレクトリパス文字列は「 / tmp / dir / target/」のようにスラッシュで終わることがよくあります。 したがって、このパス文字列を入力として使用すると、上記のすべてのアプローチは失敗します。

$ sed 's#.*/##' <<< "/tmp/dir/target/"
( empty output )

$ awk -F'/' '{print $NF}' <<< "/tmp/dir/target/"
( empty output )

$ grep -o '[^/]*$' <<< "/tmp/dir/target/"
( empty output )

$ INPUT="/tmp/dir/target/"
$ echo ${INPUT##*/}
( empty output )

わかりました。末尾のスラッシュの場合をカバーするために、上記の解決策を修正することを検討する場合があります。 次に、たとえば、awkワンライナーを少し変更して両方の場合に機能するようにします。

$ awk -F'/' '{ a = length($NF) ? $NF : $(NF-1); print a }' <<< "/tmp/dir/target"
target

$ awk -F'/' '{ a = length($NF) ? $NF : $(NF-1); print a }' <<< "/tmp/dir/target/"
target

修正されたawkワンライナーは、99% ofの場合に機能する可能性があります。 ただし、それを壊す可能性のあるエッジケースがまだあります。

次に、それらを詳しく見てみましょう。

3. コーナーケースを調べる

前のセクションでは、Linuxパス文字列がスラッシュで終わる可能性があることを学びました。 ここで、パス文字列の他の可能なパターンがあるかどうかを見てみましょう。

まず、Linuxでは、ルートディレクトリは他のすべてのファイルとディレクトリの親です。 したがって、ルートディレクトリ「/」は有効なパス文字列です。

さらに、ほとんどのLinuxファイルシステムでは、スペースをファイル名またはディレクトリ名にすることができます。 したがって、ファイルまたはディレクトリの名前が”“の場合、これは有効なパス文字列でもあります。

ここで、Linuxパス文字列(入力)と予想される結果(出力)のすべての可能なパターンを要約しましょう。

入力 期待される出力
/tmp / dir /target ターゲット
/tmp / dir / target/ ターゲット
/ /
/tmp / dir/ 」「
/tmp / dir // 」「

必要に応じて、awkワンライナーを拡張してすべてのケースをカバーすることもできます。 同様に、Bash関数もその仕事をすることができます。

ここでは、例としてawkワンライナーを示します。

$ awk -F'/' '$0==FS{ print $0; next }{ a = length($NF) ? $NF : $(NF-1); print a }' <<< "/tmp/dir/target" 
target

$ awk -F'/' '$0==FS{ print $0; next }{ a = length($NF) ? $NF : $(NF-1); print a }' <<< "/tmp/dir/target/"
target

$ awk -F'/' '$0==FS{ print $0; next }{ a = length($NF) ? $NF : $(NF-1); print a }' <<< "/" 
/

$ echo "^$( awk -F'/' '$0==FS{ print $0; next }{ a = length($NF) ? $NF : $(NF-1); print a }' <<< "/tmp/dir/    " )\$"
^    $

$ echo "^$( awk -F'/' '$0==FS{ print $0; next }{ a = length($NF) ? $NF : $(NF-1); print a }' <<< "/tmp/dir/    /" )\$" 
^    $

最後の2つの例では、「^」と「$」の間に結果を出力して、期待される結果(4つのスペース)が抽出されたことをより簡単に確認できるようにしています。 。

ご覧のとおり、awkワンライナーはすべての場合に機能します。 しかし、それを最初のバージョン( awk -F’/”{print $ NF}’ )と比較すると、今ではかなり複雑になっています。

実際、 Coreutils パッケージは、問題を解決するための便利なコマンドを提供しています。

4. basenameコマンドの使用

名前が示すように、 basename コマンドは、指定されたパス文字列の親ディレクトリを削除できます。 

さらに、それはかなり安定しており、すべてのコーナーケースをカバーしています。 次に、さまざまな入力を使用してテストを実行しましょう。

$ basename "/tmp/dir/target" 
target

$ basename "/tmp/dir/target/" 
target

$ basename "/" 
/

$ echo "^$(basename '/tmp/dir/    ')\$"
^    $

$ echo "^$(basename '/tmp/dir/    /')\$"
^    $

上記の出力が示すように、basenameコマンドは問題の簡単な解決策です。

basenameコマンドには兄弟dirnameがあり、これは逆になります—指定されたパス文字列から最後のコンポーネントを削除します

$ dirname "/tmp/dir/target" 
/tmp/dir

パス文字列を処理する必要がある場合、最初にbasenameやdirnameで問題を解決できるかどうかを検討できます。 通常、これら2つのコマンドを使用したソリューションは安定しており、理解しやすくなっています。

awk は強力なユーティリティであり、問題を確実に解決できます。 ただし、awkの実装がすべてのコーナーケースをカバーしているかどうかを考える必要があります。 そうしないと、特にスクリプトの一部である場合に、ソリューションによって予期しない結果が生じる可能性があります。

5. 結論

この記事では、問題を調査しました。指定されたパス文字列から最後のコンポーネントを抽出します。

単純な問題にはいくつかのコーナーケースがあります。 これらすべてのコーナーケースをカバーするawkワンライナーソリューションを見てきました。

また、basenameコマンドを使用するというより簡単な解決策についても説明しました。