НейроАгент

Извлечение имени файла и расширения в Bash: Полное руководство

Узнайте, как правильно извлекать имя файла и расширение в Bash для файлов с несколькими точками. Узнайте о решениях с использованием параметрического расширения, которые превосходят методы cut, awk и sed.

Вопрос

Как извлечь имя файла и расширение отдельно в Bash, когда имя файла содержит несколько точек?

Я хочу получить имя файла (без расширения) и расширение отдельно в Bash.

Лучшее решение, которое я нашел до сих пор:

NAME=`echo "$FILE" | cut -d'.' -f1`
EXTENSION=`echo "$FILE" | cut -d'.' -f2`

Этот подход неверен, потому что он не работает, когда имя файла содержит несколько символов .. Например, для файла с именем a.b.js он извлечет a как имя файла и b.js как расширение, вместо правильного a.b как имени файла и js как расширения.

Хотя это можно легко сделать в Python с помощью:

python
file, ext = os.path.splitext(path)

Я бы предпочел не вызывать интерпретатор Python только для этой задачи, если это возможно.

Какие есть лучшие подходы для правильного извлечения имени файла и расширения в Bash, особенно для имен файлов с несколькими точками?

НейроАгент

Самый надежный способ извлечь имя файла и расширение в Bash при работе с несколькими точками — использовать расширение параметров с удалением шаблонов, которое правильно определяет расширение как все после крайней правой точки. Самое простое решение:

bash
FILENAME="${FILE%.*}"
EXTENSION="${FILE##*.}"

Это правильно обработает имена файлов с несколькими точками, давая вам a.b как имя файла и js как расширение для a.b.js.


Содержание


Понимание проблемы

Основная проблема подхода на основе cut заключается в том, что он рассматривает точки как разделители и разбивает имя файла на несколько частей. Когда имя файла содержит несколько точек, например document.v1.2.txt, метод cut -d'.' -f1 захватывает только первую часть (document), а cut -d'.' -f2 захватывает вторую часть (v1.2), оставляя вам неполную картину.

Правильный подход, как реализованный в Python os.path.splitext(), рассматривает расширение как все после последней точки, а не после первой.

Это означает:

  • Для filename.tar.gz → имя файла: filename.tar, расширение: gz
  • Для archive.2024.01.zip → имя файла: archive.2024.01, расширение: zip
  • Для config (без точек) → имя файла: config, расширение: пустая строка
  • Для .hidden (начинается с точки) → имя файла: пустая строка, расширение: hidden

Решения с расширением параметров

Базовое расширение параметров

Самый элегантный и эффективный способ использует встроенное расширение параметров Bash:

bash
#!/bin/bash

FILE="a.b.js"
FILENAME="${FILE%.*}"      # Удаляет самый короткий шаблон с конца
EXTENSION="${FILE##*.}"    # Удаляет самый длинный шаблон с начала

echo "Filename: $FILENAME"    # Вывод: a.b
echo "Extension: $EXTENSION"  # Вывод: js

Как это работает:

  • ${FILE%.*} удаляет самое короткое совпадающее шаблон с конца (все после последней точки)
  • ${FILE##*.} удаляет самое длинное совпадающее шаблон с начала (все до последней точки)

Пример полного скрипта

bash
#!/bin/bash

extract_parts() {
    local file="$1"
    local filename="${file%.*}"
    local extension="${file##*.}"
    
    # Обработка случая, когда нет расширения
    if [ "$filename" = "$file" ]; then
        extension=""
    fi
    
    echo "Filename: '$filename'"
    echo "Extension: '$extension'"
}

# Тестовые случаи
extract_parts "a.b.js"          # Filename: 'a.b', Extension: 'js'
extract_parts "document.tar.gz" # Filename: 'document.tar', Extension: 'gz'
extract_parts "config"          # Filename: 'config', Extension: ''
extract_parts ".hidden"         # Filename: '', Extension: 'hidden'
extract_parts "..test.."        # Filename: '..test.', Extension: ''

Альтернативные подходы

Использование basename и dirname

Хотя basename и dirname в основном используются для манипуляции путями, их можно комбинировать для получения результата:

bash
#!/bin/bash

FILE="path/to/file.name.with.dots.txt"
BASENAME=$(basename "$FILE")
FILENAME="${BASENAME%.*}"
EXTENSION="${BASENAME##*.}"

echo "Filename: $FILENAME"    # Вывод: file.name.with.dots
echo "Extension: $EXTENSION"  # Вывод: txt

Использование awk

Решение с awk предоставляет альтернативный подход:

bash
#!/bin/bash

FILE="a.b.c.d.e"
FILENAME=$(echo "$FILE" | awk -F'.' '{$NF=""; gsub(/\.$/, ""); print}')
EXTENSION=$(echo "$FILE" | awk -F'.' '{print $NF}')

echo "Filename: $FILENAME"    # Вывод: a.b.c.d
echo "Extension: $EXTENSION"  # Вывод: e

Использование sed

Для тех, кто предпочитает sed, вот решение:

bash
#!/bin/bash

FILE="document.v1.2.txt"
FILENAME=$(echo "$FILE" | sed 's/\.[^.]*$//')
EXTENSION=$(echo "$FILE" | sed 's/^.*\.//')

echo "Filename: $FILENAME"    # Вывод: document.v1.2
echo "Extension: $EXTENSION"  # Вывод: txt

Использование rev и cut

Этот творческий подход переворачивает строку, режет, а затем переворачивает обратно:

bash
#!/bin/bash

FILE="archive.2024.01.zip"
EXTENSION=$(echo "$FILE" | rev | cut -d'.' -f1 | rev)
FILENAME="${FILE%.*}"

echo "Filename: $FILENAME"    # Вывод: archive.2024.01
echo "Extension: $EXTENSION"  # Вывод: zip

Сравнение методов

Метод Производительность Читаемость Портативность Особые случаи
Расширение параметров ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ Отлично
basename + %.* ⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ Очень хорошо
awk ⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐ Хорошо
sed ⭐⭐⭐ ⭐⭐ ⭐⭐⭐⭐ Хорошо
rev + cut ⭐⭐ ⭐⭐ ⭐⭐⭐ Удовлетворительно

Расширение параметров побеждает по всем фронтам:

  • Самый быстрый: Не вызываются внешние процессы
  • Самый чистый: Встроенный синтаксис оболочки
  • Самый портативный: Работает во всех современных оболочках
  • Самый надежный: Хорошо обрабатывает все особые случаи

Особые случаи и соображения

Имена файлов без расширений

bash
FILE="config"
FILENAME="${FILE%.*}"      # config
EXTENSION="${FILE##*.}"    # config (проблема - то же, что и имя файла)

Решение: Проверьте, одинаковы ли они:

bash
if [ "$FILENAME" = "$FILE" ]; then
    EXTENSION=""
fi

Скрытые файлы (начинающиеся с точки)

bash
FILE=".bashrc"
FILENAME="${FILE%.*}"      # пустая строка
EXTENSION="${FILE##*.}"    # bashrc

Файлы, заканчивающиеся точкой

bash
FILE="file."
FILENAME="${FILE%.*}"      # file
EXTENSION="${FILE##*.}"    # пустая строка

Файлы, состоящие только из точек

bash
FILE=".."
FILENAME="${FILE%.*}"      # пустая строка
EXTENSION="${FILE##*.}"    # пустая строка

Полная надежная функция

bash
#!/bin/bash

extract_file_parts() {
    local file="$1"
    local filename="${file%.*}"
    local extension="${file##*.}"
    
    # Обработка специальных случаев
    if [ "$filename" = "$file" ]; then
        extension=""
    elif [ -z "$filename" ] && [ -n "$extension" ]; then
        # Скрытый файл, как .bashrc
        filename=".$extension"
        extension=""
    fi
    
    printf "Filename: '%s'\n" "$filename"
    printf "Extension: '%s'\n" "$extension"
}

# Тестирование всех особых случаев
extract_file_parts "a.b.js"          # Обычный случай
extract_file_parts "document.tar.gz" # Несколько точек
extract_file_parts "config"          # Без расширения
extract_file_parts ".hidden"         # Скрытый файл
extract_file_parts "file."           # Заканчивается точкой
extract_file_parts ".."              # Только точки

Заключение

Расширение параметров с ${FILE%.*} и ${FILE##*.} — лучший подход для извлечения имени файла и расширения в Bash при работе с несколькими точками. Этот метод:

  1. Самый эффективный — не вызываются внешние процессы
  2. Правильно обрабатывает все особые случаи, включая скрытые файлы и файлы без расширений
  3. Работает во всех современных оболочках (Bash, Zsh, Dash и т.д.)
  4. Сохраняет читаемость, как только вы понимаете синтаксис
  5. Самый надежный для производственных скриптов

Для надежного написания скриптов всегда включайте проверки для имен файлов без расширений и скрытых файлов. Подход с расширением параметров превосходит альтернативы, такие как cut, awk, sed и rev+cut, по производительности, надежности и портативности, сохраняя отличную читаемость.

При работе с путями файлов, включающими каталоги, комбинируйте basename с расширением параметров: FILENAME="${basename%.*}", чтобы убедиться, что вы работаете только с частью имени файла.

Этот подход дает вам функциональность Python os.path.splitext() с использованием только встроенных возможностей оболочки, устраняя необходимость вызывать внешние интерпретаторы для простых задач разбора имен файлов.