できない.dev

ファイルの拡張子を取り出すには

ファイル名から拡張子を取り出す基本形を各言語で示す。
ドットを含めて返すかどうか、.bashrc のようにドットで始まる名前や .tar.gz のような二重拡張子をどう扱うかが言語ごとに違うので、その違いまで含めて扱う。

公開:

各言語見出しの横のバッジは検証状態を表す。実行確認済みはコードを実際に実行して確認したもの、静的確認は構文と公式 API ドキュメントで確認したものである。

Python 実行確認済み

from pathlib import Path
import os
 
p = Path("logs/archive.tar.gz")
print(p.suffix)      # .gz
print(p.suffixes)    # ['.tar', '.gz']
print(p.stem)        # archive.tar
 
print(repr(Path(".bashrc").suffix))               # ''
print(os.path.splitext("report.CSV")[1].lower())  # .csv

Path.suffix は最後のドット以降をドット付きで返し、.tar.gz のような二重拡張子をまとめて見たいときは suffixes を使う。
.bashrc のようにドットで始まるだけの名前は拡張子なしとして扱われる。
拡張子で振り分けるときは、大文字小文字をそろえてから比べる。

JavaScript 実行確認済み

import path from "node:path";
 
console.log(path.extname("logs/archive.tar.gz"));          // .gz
console.log(path.basename("logs/archive.tar.gz", ".gz"));  // archive.tar
console.log(JSON.stringify(path.extname(".bashrc")));      // ""
console.log(JSON.stringify(path.extname("README")));       // ""
 
// URL から取り出すときは、クエリを含めないよう pathname に対して使う
const url = new URL("https://example.com/img/photo.JPG?w=200");
console.log(path.extname(url.href));                          // .JPG?w=200
console.log(path.posix.extname(url.pathname).toLowerCase());  // .jpg

path.extname は最後のドット以降をドット付きで返し、拡張子が無い名前や .bashrc のようにドットで始まるだけの名前では空文字を返す。
URL 文字列にそのまま使うとクエリ文字列まで拡張子に含まれてしまうので、URL オブジェクトの pathname から取り出す。

TypeScript 実行確認済み

import { basename, extname } from "node:path";
 
const IMAGE_EXTS: ReadonlySet<string> = new Set([".png", ".jpg", ".jpeg"]);
 
function isImage(fileName: string): boolean {
  return IMAGE_EXTS.has(extname(fileName).toLowerCase());
}
 
console.log(isImage("photo.JPG"));        // true
console.log(isImage("archive.tar.gz"));   // false
console.log(basename("photo.JPG", extname("photo.JPG"))); // photo

使い方は JavaScript と同じで、extname の戻り値は string になる。
許可する拡張子を ReadonlySet にまとめておくと、判定の条件を 1 か所で管理できる。
toLowerCase で大文字小文字をそろえてから比べないと、photo.JPG のような名前を取りこぼす。

Go 実行確認済み

package main
 
import (
	"fmt"
	"path/filepath"
	"strings"
)
 
func main() {
	name := "logs/archive.tar.gz"
	ext := filepath.Ext(name)
	fmt.Println(ext)                                          // .gz
	fmt.Println(strings.TrimSuffix(filepath.Base(name), ext)) // archive.tar
 
	// ドットで始まる名前は、名前全体が拡張子として返る
	fmt.Println(filepath.Ext(".bashrc"))                     // .bashrc
	fmt.Println(strings.ToLower(filepath.Ext("report.CSV"))) // .csv
}

filepath.Ext は最後のドット以降をドット付きで返す。
拡張子を除いた名前を返す関数は無いので、Base で取り出したファイル名から strings.TrimSuffix で取り除く。
.bashrc のようにドットで始まる名前は、Python や Node.js と違って名前全体が拡張子になる。

Rust 実行確認済み

use std::path::Path;
 
fn main() {
    let p = Path::new("logs/archive.tar.gz");
    println!("{:?}", p.extension()); // Some("gz")
    println!("{:?}", p.file_stem()); // Some("archive.tar")
    println!("{:?}", Path::new(".bashrc").extension()); // None
 
    let is_csv = Path::new("report.CSV")
        .extension()
        .and_then(|e| e.to_str())
        .map_or(false, |e| e.eq_ignore_ascii_case("csv"));
    println!("{is_csv}"); // true
}

extension はドットを含まない拡張子を Option<&OsStr> で返し、拡張子が無い名前や .bashrc のようにドットで始まるだけの名前では None になる。
OsStr は OS のファイル名そのものなので、文字列として比べる前に to_str で &str に変換する。

Java 実行確認済み

import java.nio.file.Path;
 
public class FileExtension {
    static String extension(Path path) {
        String name = path.getFileName().toString();
        int dot = name.lastIndexOf('.');
        // ドットが無い、または先頭にだけある(.bashrc)なら拡張子なし
        return dot <= 0 ? "" : name.substring(dot + 1);
    }
 
    public static void main(String[] args) {
        System.out.println(extension(Path.of("logs/archive.tar.gz")));    // gz
        System.out.println(extension(Path.of(".bashrc")).isEmpty());     // true
        System.out.println(extension(Path.of("v1.2/README")).isEmpty()); // true
    }
}

Java の標準 API には拡張子を返すメソッドが無いため、ファイル名の最後のドットで切り出す。
ディレクトリ名にドットを含むパス(v1.2/README)を誤判定しないよう、先に getFileName でファイル名だけにしてから探す。

C# 実行確認済み

using System.IO;
 
Console.WriteLine(Path.GetExtension("logs/archive.tar.gz"));                // .gz
Console.WriteLine(Path.GetFileNameWithoutExtension("logs/archive.tar.gz")); // archive.tar
// ドットで始まる名前は、名前全体が拡張子として返る
Console.WriteLine(Path.GetExtension(".bashrc"));                            // .bashrc
Console.WriteLine(Path.GetExtension("README") == "");                       // True
 
bool isCsv = string.Equals(Path.GetExtension("report.CSV"), ".csv", StringComparison.OrdinalIgnoreCase);
Console.WriteLine(isCsv); // True

Path.GetExtension はドット付きで返し、拡張子が無ければ空文字を返す。
Go と同じく、.bashrc のようにドットで始まる名前は名前全体が拡張子になる。
比較に StringComparison.OrdinalIgnoreCase を使うと、ToLower で新しい文字列を作らずに大文字小文字を無視できる。

つまずき

.bashrc のようにドットで始まる名前の扱いが言語で割れる。
Python の pathlib、Node.js の path.extname、Rust の extension は拡張子なしとみなすが、Go の filepath.Ext と C# の Path.GetExtension は名前全体を拡張子として返す。
隠しファイルを拡張子で振り分ける処理を言語をまたいで移植すると、.bashrc が「拡張子 bashrc のファイル」として扱われる。

ドットを含むかどうか

Python・JavaScript・TypeScript・Go・C# は「.gz」のようにドット付きで返し、Rust の extension と上の Java の例は「gz」とドットなしで返す。
許可リストや比較対象の文字列を書くときは、使っている関数がどちらを返すのかに合わせる。

二重拡張子と、拡張子を信用しすぎないこと

archive.tar.gz のような名前では、どの言語も最後の .gz だけを返す。
.tar.gz をまとめて判定したいときは、Python の suffixes を使うか、ファイル名が .tar.gz で終わるかを直接調べる。
また拡張子はファイル名の一部にすぎないので、アップロードされたファイルの種類を判断するときは、拡張子だけでなく中身も確かめる。

この記事は役立ちましたか?