web-dev-qa-db-ja.com

Bashで拡張子なしのファイル名を取得する

私は次のforループを使用して、フォルダー内のすべてのテキストファイルを個別にsortします(つまり、それぞれのソートされた出力ファイルを作成します)。

for file in *.txt; 
do
   printf 'Processing %s\n' "$file"
   LC_ALL=C sort -u "$file" > "./${file}_sorted"  
done

現在、次の形式でファイルを出力することを除いて、これはほぼ完璧です。

originalfile.txt_sorted

...一方、次の形式でファイルを出力します。

originalfile_sorted.txt 

これは、${file}変数には、拡張子を含むファイル名が含まれます。 Windows上でCygwinを実行しています。これが真のLinux環境でどのように動作するかはわかりませんが、Windowsでは、この拡張子の変更により、Windowsエクスプローラーからファイルにアクセスできなくなります。

ファイル名を拡張子から分離して、_sorted 2つの間のサフィックス。Windowsのファイル拡張子をそのまま維持しながら、ファイルの元のバージョンとソートされたバージョンを簡単に区別できるようにしますか?

私は何が might になるか possible の解決策を見てきましたが、私にはこれらはより複雑な問題を処理するのにより適したように見えます。さらに重要なのは、私の現在のbashの知識により、彼らは私の頭をはるかに超えてしまうので、私の控えめなforループに適用される簡単な解決策があるか、それとも誰かがこれらのソリューションを私の状況に適用する方法を説明できます。

6
Hashim

あなたがリンクするこれらのソリューションは実際には非常に優れています。いくつかの答えは説明が足りないかもしれませんので、それを整理してみましょう。

あなたのこのライン

_for file in *.txt
_

拡張機能が事前にわかっていることを示します(注:POSIX準拠の環境では大文字と小文字が区別され、_*.txt_は_FOO.TXT_と一致しません)。このような場合には

_basename -s .txt "$file"
_

拡張子なしの名前を返す必要があります(basenameもディレクトリパスを削除します:_/directory/path/filename_→filename;あなたの場合、_$file_が含まれていないため、問題ではありませんそのようなパス)。コードでツールを使用するには、一般的に次のようなコマンド置換が必要です:$(some_command)。コマンド置換は_some_command_の出力を受け取り、それを文字列として扱い、$(…)がある場所に配置します。あなたの特定のリダイレクトは

_… > "./$(basename -s .txt "$file")_sorted.txt"
#      ^^^^^^^^^^^^^^^^^^^^^^^^^^^ the output of basename will replace this
_

Bashは$(…)内の引用符がペアになっていることを認識するのに十分スマートであるため、ここではネストされた引用符で問題ありません。

これは改善できます。注basenameは独立した実行可能ファイルであり、Shell組み込みではありません(Bashでは_type basename_を実行し、_type cd_と比較してください)。余分なプロセスを生成するにはコストがかかり、リソースと時間がかかります。ループでのスポーンは通常、パフォーマンスが低下します。したがって、余分なプロセスを回避するために、シェルが提供するものをすべて使用する必要があります。この場合の解決策は次のとおりです。

_… > "./${file%.txt}_sorted.txt"
_

より一般的な場合について、構文を以下で説明します。


拡張機能がわからない場合:

_… > "./${file%.*}_sorted.${file##*.}"
_

構文の説明:

  • _${file#*.}_ – _$file_、ただし_*._に一致する最短の文字列は前面から削除されます。
  • _${file##*.}_ – _$file_、ただし_*._に一致する最長の文字列は前面から削除されます。それを使用して拡張機能のみを取得します。
  • _${file%.*}_ – _$file_、ただし_.*_に一致する最短の文字列は末尾から削除されます。拡張機能以外のすべてを取得するために使用します。
  • _${file%%.*}_ – _$file_、ただし最長の文字列に一致する_.*_は末尾から削除されます。

パターンマッチングは正規表現ではなく、globに似ています。つまり、_*_は0文字以上のワイルドカード、_?_は1文字のワイルドカードです(ただし、_?_は必要ありません)。 _ls *.txt_または_for file in *.txt;_を呼び出すと、同じパターンマッチングメカニズムが使用されます。ワイルドカードなしのパターンを使用できます。すでに_${file%.txt}_を使用していますが、_.txt_はパターンです。

例:

_$ file=name.name2.name3.ext
$ echo "${file#*.}"
name2.name3.ext
$ echo "${file##*.}"
ext
$ echo "${file%.*}"
name.name2.name3
$ echo "${file%%.*}"
name
_

しかし注意してください:

_$ file=extensionless
$ echo "${file#*.}"
extensionless
$ echo "${file##*.}"
extensionless
$ echo "${file%.*}"
extensionless
$ echo "${file%%.*}"
extensionless
_

このため、次の仕掛けmightが役立ちます(ただし、以下の説明では役に立ちません)。

_${file#${file%.*}}
_

拡張子(_${file%.*}_)以外のすべてを識別して機能し、文字列全体からこれを削除します。結果は次のようになります。

_$ file=name.name2.name3.ext
$ echo "${file#${file%.*}}"
.ext
$ file=extensionless
$ echo "${file#${file%.*}}"

$   # empty output above
_

今回は_._が含まれていることに注意してください。 _$file_にリテラル_*_または_?_が含まれていると、予期しない結果が生じる可能性があります。しかし、Windows(拡張子が重要な場合) 許可されない とにかくファイル名にこれらの文字があるため、気にする必要はありません。ただし、_[…]_または_{…}_が存在する場合は、独自のパターンマッチングスキームがトリガーされ、ソリューションが壊れる可能性があります。

「改善された」リダイレクトは次のようになります。

_… > "./${file%.*}_sorted${file#${file%.*}}"
_

残念ながら、角かっこや中かっこは使用できませんが、拡張子の有無にかかわらずファイル名をサポートする必要があります。 かなり残念です。 これを修正するには、内部変数を二重引用符で囲む必要があります。

リダイレクトが大幅に改善されました:

_… > "./${file%.*}_sorted${file#"${file%.*}"}"
_

二重引用符は_${file%.*}_をパターンとして機能させません! Bashは、内側の引用符が外側の_${…}_構文に埋め込まれているため、内側と外側の引用符を区別できるほどスマートです。 これは正しい方法だと思います

別の(不完全な)解決策、教育上の理由でそれを分析しましょう:

_${file/./_sorted.}
_

最初の_._を__sorted._に置き換えます。 _$file_にドットが1つ以下の場合は問題なく動作します。すべてのドットを置き換える同様の構文_${file//./_sorted.}_があります。私が知る限り、lastドットのみを置き換えるバリアントはありません。

それでも、_._を含むファイルの最初のソリューションは堅牢に見えます。拡張子のない_$file_のソリューションは簡単です:_${file}_sorted_。ここで必要なのは、2つのケースを区別する方法です。ここにあります:

_[[ "$file" == *?.* ]]
_

_$file_変数の内容が右側のパターンに一致する場合にのみ、終了ステータス0(true)を返します。パターンは、「少なくとも1つの文字の後にドットがある」または同等に「先頭にないドットがある」と言います。ポイントは、どこかにanotherドットがない限り、Linux隠しファイル(例:_.bashrc_)を拡張子なしとして扱うことです。

ここでは_[[_ではなく_[_が必要であることに注意してください。前者はより強力ですが、残念ながら 移植不可 ;後者は移植可能ですが、制限が多すぎます。

ロジックは次のようになります。

_[[ "$file" == *?.* ]] && file1="./${file%.*}_sorted.${file##*.}" || file1="${file}_sorted"
_

この後、_$file1_には目的の名前が含まれるため、リダイレクトは

_… > "./$file1"
_

そして、コード全体(_*.txt_を_*_で置き換えて、拡張機能があるかどうかを示します):

_for file in *; 
do
   printf 'Processing %s\n' "$file"
   [[ "$file" == *?.* ]] && file1="./${file%.*}_sorted.${file##*.}" || file1="${file}_sorted"
   LC_ALL=C sort -u "$file" > "./$file1"  
done
_

これは、もしあればディレクトリも処理しようとします。あなたはすでに知っています 何をすべきか それを修正します。

19