Извлечь имя файла и расширение в Bash - PullRequest
1849 голосов
/ 08 июня 2009

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

Лучшее решение, которое я нашел на данный момент:

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

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

Это легко сделать на Python с помощью

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

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

Есть идеи получше?

Ответы [ 37 ]

3159 голосов
/ 08 июня 2009

Сначала получите имя файла без пути:

filename=$(basename -- "$fullfile")
extension="${filename##*.}"
filename="${filename%.*}"

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

filename="${fullfile##*/}"

Вы можете проверить документацию:

575 голосов
/ 08 июня 2009
~% FILE="example.tar.gz"
~% echo "${FILE%%.*}"
example
~% echo "${FILE%.*}"
example.tar
~% echo "${FILE#*.}"
tar.gz
~% echo "${FILE##*.}"
gz

Подробнее см. расширение параметров оболочки в руководстве по Bash.

344 голосов
/ 19 октября 2011

Обычно вы уже знаете расширение, поэтому вы можете использовать:

basename filename .extension

например:

basename /path/to/dir/filename.txt .txt

и мы получаем

filename
133 голосов
/ 05 февраля 2013

Вы можете использовать магию переменных POSIX:

bash-3.2$ FILENAME=somefile.tar.gz
bash-3.2$ echo ${FILENAME%%.*}
somefile
bash-3.2$ echo ${FILENAME%.*}
somefile.tar

Существует предостережение в том, что если ваше имя файла имеет вид ./somefile.tar.gz, то echo ${FILENAME%%.*} жадно удалит самое длинное совпадение с ., и вы получите пустую строку.

(Вы можете обойти это с помощью временной переменной:

FULL_FILENAME=$FILENAME
FILENAME=${FULL_FILENAME##*/}
echo ${FILENAME%%.*}

)


Этот сайт объясняет больше.

${variable%pattern}
  Trim the shortest match from the end
${variable##pattern}
  Trim the longest match from the beginning
${variable%%pattern}
  Trim the longest match from the end
${variable#pattern}
  Trim the shortest match from the beginning
68 голосов
/ 10 сентября 2009

Это не работает, если файл не имеет расширения или не имеет имени файла. Вот что я использую; он использует только встроенные функции и обрабатывает больше (но не все) патологических имен файлов.

#!/bin/bash
for fullpath in "$@"
do
    filename="${fullpath##*/}"                      # Strip longest match of */ from start
    dir="${fullpath:0:${#fullpath} - ${#filename}}" # Substring from 0 thru pos of filename
    base="${filename%.[^.]*}"                       # Strip shortest match of . plus at least one non-dot char from end
    ext="${filename:${#base} + 1}"                  # Substring from len of base thru end
    if [[ -z "$base" && -n "$ext" ]]; then          # If we have an extension and no base, it's really the base
        base=".$ext"
        ext=""
    fi

    echo -e "$fullpath:\n\tdir  = \"$dir\"\n\tbase = \"$base\"\n\text  = \"$ext\""
done

А вот несколько тестов:

$ basename-and-extension.sh / /home/me/ /home/me/file /home/me/file.tar /home/me/file.tar.gz /home/me/.hidden /home/me/.hidden.tar /home/me/.. .
/:
    dir  = "/"
    base = ""
    ext  = ""
/home/me/:
    dir  = "/home/me/"
    base = ""
    ext  = ""
/home/me/file:
    dir  = "/home/me/"
    base = "file"
    ext  = ""
/home/me/file.tar:
    dir  = "/home/me/"
    base = "file"
    ext  = "tar"
/home/me/file.tar.gz:
    dir  = "/home/me/"
    base = "file.tar"
    ext  = "gz"
/home/me/.hidden:
    dir  = "/home/me/"
    base = ".hidden"
    ext  = ""
/home/me/.hidden.tar:
    dir  = "/home/me/"
    base = ".hidden"
    ext  = "tar"
/home/me/..:
    dir  = "/home/me/"
    base = ".."
    ext  = ""
.:
    dir  = ""
    base = "."
    ext  = ""
42 голосов
/ 05 февраля 2013

Вы можете использовать basename.

Пример:

$ basename foo-bar.tar.gz .tar.gz
foo-bar

Вам необходимо предоставить базовое имя с расширением, которое должно быть удалено, однако, если вы всегда выполняете tar с -z, тогда вы знаете, что расширение будет .tar.gz.

Это должно делать то, что вы хотите:

tar -zxvf $1
cd $(basename $1 .tar.gz)
29 голосов
/ 08 июня 2009
pax> echo a.b.js | sed 's/\.[^.]*$//'
a.b
pax> echo a.b.js | sed 's/^.*\.//'
js

отлично работает, поэтому вы можете просто использовать:

pax> FILE=a.b.js
pax> NAME=$(echo "$FILE" | sed 's/\.[^.]*$//')
pax> EXTENSION=$(echo "$FILE" | sed 's/^.*\.//')
pax> echo $NAME
a.b
pax> echo $EXTENSION
js

Команды, кстати, работают следующим образом.

Команда для NAME заменяет символ ".", за которым следует любое количество не "." символов, до конца строки, без нуля (т. Е. Она удаляет все из последнего "." в конец строки включительно). Это в основном не жадная замена с использованием трюков с регулярными выражениями.

Команда для EXTENSION заменяет любое количество символов, за которыми следует символ "." в начале строки, ничем (т. Е. Удаляет все, начиная от начала строки и заканчивая последней точкой включительно). , Это жадная замена, действие по умолчанию.

27 голосов
/ 21 июля 2010

Меллен пишет в комментарии к сообщению в блоге:

Используя Bash, также есть ${file%.*} для получения имени файла без расширения и ${file##*.} для получения только расширения. То есть

file="thisfile.txt"
echo "filename: ${file%.*}"
echo "extension: ${file##*.}"

Выходы:

filename: thisfile
extension: txt
25 голосов
/ 05 февраля 2013

Вы можете использовать команду cut для удаления двух последних расширений (часть ".tar.gz"):

$ echo "foo.tar.gz" | cut -d'.' --complement -f2-
foo

Как отметил Клейтон Хьюз в комментарии, это не будет работать для фактического примера в вопросе. Поэтому в качестве альтернативы я предлагаю использовать sed с расширенными регулярными выражениями, например:

$ echo "mpc-1.0.1.tar.gz" | sed -r 's/\.[[:alnum:]]+\.[[:alnum:]]+$//'
mpc-1.0.1

Работает, безоговорочно удаляя два последних (буквенно-цифровых) расширения.

[Обновлено снова после комментария от Андерса Линдала]

24 голосов
/ 02 декабря 2016

Не нужно беспокоиться о awk или sed или даже perl для этой простой задачи. Существует чистое Bash, os.path.splitext() -совместимое решение, которое использует только расширения параметров.

Ссылочная реализация

Документация os.path.splitext(path):

Разбить путь к пути на пару (root, ext) так, чтобы root + ext == path и ext были пустыми или начинались с периода и содержали не более одного периода. Ведущие периоды на базовом имени игнорируются; splitext('.cshrc') возвращает ('.cshrc', '').

Код Python:

root, ext = os.path.splitext(path)

Реализация Bash

Чтение ведущих периодов

root="${path%.*}"
ext="${path#"$root"}"

Игнорирование ведущих периодов

root="${path#.}";root="${path%"$root"}${root%.*}"
ext="${path#"$root"}"

Тесты

Вот тестовые примеры для Игнорирования начальных периодов , которые должны соответствовать эталонной реализации Python на каждом входе.

|---------------|-----------|-------|
|path           |root       |ext    |
|---------------|-----------|-------|
|' .txt'        |' '        |'.txt' |
|' .txt.txt'    |' .txt'    |'.txt' |
|' txt'         |' txt'     |''     |
|'*.txt.txt'    |'*.txt'    |'.txt' |
|'.cshrc'       |'.cshrc'   |''     |
|'.txt'         |'.txt'     |''     |
|'?.txt.txt'    |'?.txt'    |'.txt' |
|'\n.txt.txt'   |'\n.txt'   |'.txt' |
|'\t.txt.txt'   |'\t.txt'   |'.txt' |
|'a b.txt.txt'  |'a b.txt'  |'.txt' |
|'a*b.txt.txt'  |'a*b.txt'  |'.txt' |
|'a?b.txt.txt'  |'a?b.txt'  |'.txt' |
|'a\nb.txt.txt' |'a\nb.txt' |'.txt' |
|'a\tb.txt.txt' |'a\tb.txt' |'.txt' |
|'txt'          |'txt'      |''     |
|'txt.pdf'      |'txt'      |'.pdf' |
|'txt.tar.gz'   |'txt.tar'  |'.gz'  |
|'txt.txt'      |'txt'      |'.txt' |
|---------------|-----------|-------|

Результаты испытаний

Все испытания пройдены.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...