Извлечь имя файла и расширение в Bash

2114

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

Лучшее решение, которое я нашел, это:

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 только для этого, если это возможно.

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

IBZ
источник
Этот вопрос объясняет эту технику bash и несколько других связанных с ней.
jjclarkson
28
Применяя замечательные ответы ниже, не просто вставляйте переменную, как показано здесь. Неправильно: extension="{$filename##*.}" как я это делал некоторое время! Переместите $наружу кудряшки: Справа: extension="${filename##*.}"
Крис К
4
Это явно нетривиальная проблема, и мне трудно сказать, являются ли приведенные ниже ответы полностью правильными. Удивительно, что это не встроенная операция в (ba) sh (ответы, кажется, реализуют функцию, используя сопоставление с образцом). os.path.splitextВместо этого я решил использовать Python, как указано выше ...
Питер Гибсон
1
Поскольку расширение должно представлять природу файла, существует волшебная команда, которая проверяет файл, чтобы определить его природу и предложить стандартное расширение . смотри мой ответ
Ф. Хаури
2
Вопрос, в первую очередь, проблематичен, потому что ... С точки зрения файловых систем ОС и unix вообще нет такой вещи, как расширение файла. Используя "." разделять части - это человеческое соглашение , которое работает только до тех пор, пока люди соглашаются следовать ему. Например, с помощью программы «tar» можно было бы назвать имена выходных файлов «tar». префикс вместо суффикса ".tar" - дает "tar.somedir" вместо "somedir.tar". Из-за этого не существует решения «общее, всегда работает» - вы должны написать код, который соответствует вашим конкретным потребностям и ожидаемым именам файлов.
СМ

Ответы:

3508

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

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

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

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

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

Petesh
источник
85
Проверьте gnu.org/software/bash/manual/html_node/… для полного набора функций.
Д.Шоули
24
Добавьте кавычки в $ fullfile, иначе вы рискуете сломать имя файла.
лунат
47
Черт возьми, вы могли бы даже написать имя файла = "$ {FullFile ## * /}" и не вызывать дополнительныйbasename
ephemient
45
Это «решение» не работает, если файл не имеет расширения - вместо этого выводится полное имя файла, что очень плохо, учитывая, что файлы без расширений вездесущи.
NCCC
43
Фикс для работы с именами файлов без расширения: extension=$([[ "$filename" = *.* ]] && echo ".${filename##*.}" || echo ''). Обратите внимание , что если расширение является присутствует, то он будет возвращен в том числе начального ., например, .txt.
mklement0
685
~% FILE="example.tar.gz"

~% echo "${FILE%%.*}"
example

~% echo "${FILE%.*}"
example.tar

~% echo "${FILE#*.}"
tar.gz

~% echo "${FILE##*.}"
gz

Для получения дополнительной информации см. Расширение параметров оболочки в руководстве по Bash.

Жулиано
источник
22
Вы (возможно, непреднамеренно) поднимаете отличный вопрос о том, что делать, если в «расширенной» части имени файла есть 2 точки, как в .tar.gz ... Я никогда не рассматривал эту проблему, и я подозреваю, что это невозможно решить, не зная заранее всех возможных допустимых расширений файлов.
rmeador
8
Почему не решаемо? В моем примере следует учитывать, что файл содержит два расширения, а не расширение с двумя точками. Вы обрабатываете оба расширения отдельно.
Джулиано
22
Это неразрешимо на лексической основе, вам нужно проверить тип файла. Подумайте, была ли у вас игра под названием, dinosaurs.in.tarи вы ее разархивировали dinosaurs.in.tar.gz:)
porges
11
Это становится сложнее, если вы проходите полными путями. У одного из моих было «.» в каталоге в середине пути, но не в имени файла. Пример "a / bc / d / e / filename" приведет к завершению ".c / d / e / filename"
Уолт Селлерс
7
явно нет x.tar.gzрасширения gzи имя файла таково x.tar. Двойных расширений не существует. Я уверен, что Boost :: Filesystem справится с этим. (split path, change_extension ...) и его поведение основано на python, если я не ошибаюсь.
v.oddou
432

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

basename filename .extension

например:

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

и мы получаем

filename
Томи По
источник
61
Этот второй аргумент basenameдовольно откровенен, ты добр, сэр / мадам :)
akaIDIOT
10
А как извлечь расширение, используя эту технику? ;) Ой, подожди! Мы на самом деле не знаем этого заранее.
Томаш Гандор
3
Скажем, у вас есть каталог, который либо заканчивается либо, .zipлибо .ZIP. Есть ли способ сделать что-то подобное basename $file {.zip,.ZIP}?
Деннис
8
Хотя это отвечает только на часть вопроса ОП, оно отвечает на вопрос, который я ввел в Google. :-) Очень гладко!
sudo make установить
1
простой и POSIX-совместимый
gpanda
147

Вы можете использовать магию расширения параметров 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
sotapme
источник
5
Намного проще, чем ответ Иоахима, но мне всегда нужно искать подстановку переменных POSIX. Кроме того, это работает на Max OSX, где cutнет --complementи sedне имеет -r.
jwadsack
72

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

#!/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 = "/"
    база = ""
    ext = ""
/ Главная / мне /:
    dir = "/ home / me /"
    база = ""
    ext = ""
/ Главная / мне / файл:
    dir = "/ home / me /"
    база = "файл"
    ext = ""
/home/me/file.tar:
    dir = "/ home / me /"
    база = "файл"
    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"
/ Главная / мне / ..:
    dir = "/ home / me /"
    база = ".."
    ext = ""
.:
    dir = ""
    база = "."
    ext = ""
Доктор Дж
источник
2
Вместо того, чтобы dir="${fullpath:0:${#fullpath} - ${#filename}}"я часто видел dir="${fullpath%$filename}". Проще написать. Не уверен, есть ли реальная разница в скорости или ошибки.
dubiousjim
2
Это использует #! / Bin / bash, что почти всегда неверно. Предпочитайте #! / Bin / sh, если это возможно, или #! / Usr / bin / env bash, если нет.
Хороший человек
@ Хороший человек: я не знаю, как это почти всегда неправильно: which bash-> /bin/bash; возможно это твой дистрибутив?
Vol7ron
2
@ vol7ron - на многих дистрибутивах bash находится в / usr / local / bin / bash. В OSX многие люди устанавливают обновленный bash в / opt / local / bin / bash. Таким образом, / bin / bash неверен, и для его поиска нужно использовать env. Еще лучше использовать конструкции / bin / sh и POSIX. За исключением соляриса, это оболочка POSIX.
Хороший человек
2
@ GoodPerson, но если вам удобнее работать с bash, зачем использовать sh? Разве это не значит говорить, зачем использовать Perl, если вы можете использовать sh?
vol7ron
46

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

Пример:

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

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

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

tar -zxvf $1
cd $(basename $1 .tar.gz)
Бьярке Фрейнд-Хансен
источник
2
Я полагаю, cd $(basename $1 .tar.gz)работает для файлов .gz. Но в вопросе он упомянулArchive files have several extensions: tar.gz, tat.xz, tar.bz2
SS Hegde
Томи По опубликовал то же самое за 2 года до этого.
phil294
Привет Блаухирн, вау, это старые вопросы. Я думаю, что-то случилось с датами. Я отчетливо помню, как отвечал на вопрос вскоре после того, как он был задан, и там, где только пара других ответов. Может ли быть так, что вопрос был объединен с другим, делает ли это ТАК?
Бьярке Фрейнд-Хансен
Да, я правильно помню. Первоначально я отвечаю на этот вопрос stackoverflow.com/questions/14703318/… в тот же день, когда его спросили, через 2 года он был объединен с этим. Вряд ли меня обвинят в дублировании ответа, когда мой ответ был перемещен таким образом.
Бьярке Фрейнд-Хансен
38
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замены любого числа символов, за которыми следует "."символ в начале строки, ничем (т. Е. Она удаляет все от начала строки до конечной точки включительно). Это жадная замена, которая является действием по умолчанию.

paxdiablo
источник
Этот разрыв для файлов без расширения, поскольку он будет печатать то же самое для имени и расширения. Поэтому я использую sed 's,\.[^\.]*$,,'для имени и sed 's,.*\.,., ;t ;g'для расширения (использует нетипичные testи getкоманды, наряду с типичной substituteкомандой).
СЧАСТЛИВЫЙ
32

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

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

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

Выходы:

filename: thisfile
extension: txt
Kebabbert
источник
2
@REACHUS: См gnu.org/software/bash/manual/html_node/...
mklement0
29

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

Реализация ссылок

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

Разделить путь имени пути в пару (root, ext)таких , что root + ext == pathи внутр пуст или начинается с периодом и содержит не более одного периода. Ведущие периоды на базовом имени игнорируются; 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' |
|---------------|-----------|-------|

Результаты теста

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

Cyker
источник
2
нет, базовое имя файла для text.tar.gzдолжно быть textи расширение быть.tar.gz
frederick99
2
@ frederick99 Как я уже сказал, решение здесь соответствует реализации os.path.splitextв Python. Является ли эта реализация вменяемой для возможных противоречивых мнений - это еще одна тема.
Cyker
Как работают кавычки в шаблоне ( "$root")? Что может произойти, если они были опущены? (Я не смог найти никакой документации по этому вопросу.) Кроме того, как это обрабатывает имена файлов с *или ?в них?
Ymett
Хорошо, тестирование показывает мне, что кавычки делают шаблон буквальным, то есть *и ?не являются специальными. Таким образом, две части моего вопроса отвечают друг другу. Я прав, что это не задокументировано? Или это следует понимать из того факта, что кавычки отключают глобальное расширение вообще?
Ymett
Блестящий ответ! Я просто предложу немного более простой вариант для вычисления корня: root="${path#?}";root="${path::1}${root%.*}"- затем выполните то же самое, чтобы извлечь расширение.
Maëlan
26

Вы можете использовать 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

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

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

Какой-то программист чувак
источник
4
Это работает только в том случае, если имя файла / путь не содержит никаких других точек: echo "mpc-1.0.1.tar.gz" | cut -d '.' --complement -f2- создает «mpc-1» (только первые 2 поля после разделителя.)
Клейтон Хьюз
@ClaytonHughes Вы правы, и я должен был проверить это лучше. Добавлено другое решение.
Какой-то программист чувак
Выражения sed следует использовать $для проверки того, что соответствующее расширение находится в конце имени файла. В противном случае имя файла i.like.tar.gz.files.tar.bz2может привести к неожиданному результату.
Андерс Линдал
@AndersLindahl Это все еще будет, если порядок расширений обратен sedпорядку цепочек. Даже $в конце имя файла, такое как mpc-1.0.1.tar.bz2.tar.gzудалит оба, .tar.gzа затем .tar.bz2.
Какой-то программист чувак
$ echo "foo.tar.gz" | cut -d '.' -f2- БЕЗ --complement получит 2-й разделенный элемент до конца строки $ echo "foo.tar.gz" | cut -d '.' -f2- tar.gz
Джин Блэк
23

Вот несколько альтернативных предложений (в основном, в awk), включая некоторые расширенные варианты использования, такие как извлечение номеров версий для пакетов программного обеспечения.

f='/path/to/complex/file.1.0.1.tar.gz'

# Filename : 'file.1.0.x.tar.gz'
    echo "$f" | awk -F'/' '{print $NF}'

# Extension (last): 'gz'
    echo "$f" | awk -F'[.]' '{print $NF}'

# Extension (all) : '1.0.1.tar.gz'
    echo "$f" | awk '{sub(/[^.]*[.]/, "", $0)} 1'

# Extension (last-2): 'tar.gz'
    echo "$f" | awk -F'[.]' '{print $(NF-1)"."$NF}'

# Basename : 'file'
    echo "$f" | awk '{gsub(/.*[/]|[.].*/, "", $0)} 1'

# Basename-extended : 'file.1.0.1.tar'
    echo "$f" | awk '{gsub(/.*[/]|[.]{1}[^.]+$/, "", $0)} 1'

# Path : '/path/to/complex/'
    echo "$f" | awk '{match($0, /.*[/]/, a); print a[0]}'
    # or 
    echo "$f" | grep -Eo '.*[/]'

# Folder (containing the file) : 'complex'
    echo "$f" | awk -F'/' '{$1=""; print $(NF-1)}'

# Version : '1.0.1'
    # Defined as 'number.number' or 'number.number.number'
    echo "$f" | grep -Eo '[0-9]+[.]+[0-9]+[.]?[0-9]?'

    # Version - major : '1'
    echo "$f" | grep -Eo '[0-9]+[.]+[0-9]+[.]?[0-9]?' | cut -d. -f1

    # Version - minor : '0'
    echo "$f" | grep -Eo '[0-9]+[.]+[0-9]+[.]?[0-9]?' | cut -d. -f2

    # Version - patch : '1'
    echo "$f" | grep -Eo '[0-9]+[.]+[0-9]+[.]?[0-9]?' | cut -d. -f3

# All Components : "path to complex file 1 0 1 tar gz"
    echo "$f" | awk -F'[/.]' '{$1=""; print $0}'

# Is absolute : True (exit-code : 0)
    # Return true if it is an absolute path (starting with '/' or '~/'
    echo "$f" | grep -q '^[/]\|^~/'

Во всех случаях использования в качестве входных данных используется исходный полный путь, не зависящий от промежуточных результатов.

henfiber
источник
20

Общепринятый ответ хорошо работает в типичных случаях , но не может в крайних случаях , а именно:

  • Для имен файлов без расширения (называемых суффиксом в оставшейся части этого ответа) extension=${filename##*.}возвращается имя входного файла, а не пустая строка.
  • extension=${filename##*.}не включает в себя начальные ., вопреки соглашению.
    • Слепое предисловие .не будет работать для имен файлов без суффикса.
  • filename="${filename%.*}"будет пустой строкой, если имя входного файла начинается с .и не содержит дополнительных .символов (например, .bash_profile) - вопреки соглашению.

---------

Таким образом, сложность надежного решения, охватывающего все граничные случаи, требует функции - см. Ее определение ниже; он может вернуть все компоненты пути .

Пример вызова:

splitPath '/etc/bash.bashrc' dir fname fnameroot suffix
# -> $dir == '/etc'
# -> $fname == 'bash.bashrc'
# -> $fnameroot == 'bash'
# -> $suffix == '.bashrc'

Обратите внимание, что аргументы после входного пути выбираются свободно, имена позиционных переменных .
Чтобы пропустить не представляющие интереса переменные, которые предшествуют тем, которые есть, укажите _(для использования одноразовой переменной $_) или ''; например, чтобы извлечь только имя файла и расширение, используйте splitPath '/etc/bash.bashrc' _ _ fnameroot extension.


# SYNOPSIS
#   splitPath path varDirname [varBasename [varBasenameRoot [varSuffix]]] 
# DESCRIPTION
#   Splits the specified input path into its components and returns them by assigning
#   them to variables with the specified *names*.
#   Specify '' or throw-away variable _ to skip earlier variables, if necessary.
#   The filename suffix, if any, always starts with '.' - only the *last*
#   '.'-prefixed token is reported as the suffix.
#   As with `dirname`, varDirname will report '.' (current dir) for input paths
#   that are mere filenames, and '/' for the root dir.
#   As with `dirname` and `basename`, a trailing '/' in the input path is ignored.
#   A '.' as the very first char. of a filename is NOT considered the beginning
#   of a filename suffix.
# EXAMPLE
#   splitPath '/home/jdoe/readme.txt' parentpath fname fnameroot suffix
#   echo "$parentpath" # -> '/home/jdoe'
#   echo "$fname" # -> 'readme.txt'
#   echo "$fnameroot" # -> 'readme'
#   echo "$suffix" # -> '.txt'
#   ---
#   splitPath '/home/jdoe/readme.txt' _ _ fnameroot
#   echo "$fnameroot" # -> 'readme'  
splitPath() {
  local _sp_dirname= _sp_basename= _sp_basename_root= _sp_suffix=
    # simple argument validation
  (( $# >= 2 )) || { echo "$FUNCNAME: ERROR: Specify an input path and at least 1 output variable name." >&2; exit 2; }
    # extract dirname (parent path) and basename (filename)
  _sp_dirname=$(dirname "$1")
  _sp_basename=$(basename "$1")
    # determine suffix, if any
  _sp_suffix=$([[ $_sp_basename = *.* ]] && printf %s ".${_sp_basename##*.}" || printf '')
    # determine basename root (filemane w/o suffix)
  if [[ "$_sp_basename" == "$_sp_suffix" ]]; then # does filename start with '.'?
      _sp_basename_root=$_sp_basename
      _sp_suffix=''
  else # strip suffix from filename
    _sp_basename_root=${_sp_basename%$_sp_suffix}
  fi
  # assign to output vars.
  [[ -n $2 ]] && printf -v "$2" "$_sp_dirname"
  [[ -n $3 ]] && printf -v "$3" "$_sp_basename"
  [[ -n $4 ]] && printf -v "$4" "$_sp_basename_root"
  [[ -n $5 ]] && printf -v "$5" "$_sp_suffix"
  return 0
}

test_paths=(
  '/etc/bash.bashrc'
  '/usr/bin/grep'
  '/Users/jdoe/.bash_profile'
  '/Library/Application Support/'
  'readme.new.txt'
)

for p in "${test_paths[@]}"; do
  echo ----- "$p"
  parentpath= fname= fnameroot= suffix=
  splitPath "$p" parentpath fname fnameroot suffix
  for n in parentpath fname fnameroot suffix; do
    echo "$n=${!n}"
  done
done

Тестовый код, который выполняет функцию:

test_paths=(
  '/etc/bash.bashrc'
  '/usr/bin/grep'
  '/Users/jdoe/.bash_profile'
  '/Library/Application Support/'
  'readme.new.txt'
)

for p in "${test_paths[@]}"; do
  echo ----- "$p"
  parentpath= fname= fnameroot= suffix=
  splitPath "$p" parentpath fname fnameroot suffix
  for n in parentpath fname fnameroot suffix; do
    echo "$n=${!n}"
  done
done

Ожидаемый результат - обратите внимание на крайние случаи:

  • имя файла без суффикса
  • имя файла, начинающееся с .( не считается началом суффикса)
  • входной путь, заканчивающийся на /(трейлинг /игнорируется)
  • входной путь, который является только именем файла ( .возвращается как родительский путь)
  • имя файла с .токеном с префиксом (суффикс считается только последний):
----- /etc/bash.bashrc
parentpath=/etc
fname=bash.bashrc
fnameroot=bash
suffix=.bashrc
----- /usr/bin/grep
parentpath=/usr/bin
fname=grep
fnameroot=grep
suffix=
----- /Users/jdoe/.bash_profile
parentpath=/Users/jdoe
fname=.bash_profile
fnameroot=.bash_profile
suffix=
----- /Library/Application Support/
parentpath=/Library
fname=Application Support
fnameroot=Application Support
suffix=
----- readme.new.txt
parentpath=.
fname=readme.new.txt
fnameroot=readme.new
suffix=.txt
mklement0
источник
19

Наименьшее и простое решение (в одну строку) это:

$ file=/blaabla/bla/blah/foo.txt
echo $(basename ${file%.*}) # foo
Рон
источник
Это бесполезное использованиеecho . В общем случае, echo $(command)лучше писать просто, commandесли только вы специально не требуете, чтобы оболочка выполнила токенизацию пробелов и расширение подстановочных знаков в выходных данных commandперед отображением результата. Тест: каков результат echo $(echo '*')(и если это то, что вы действительно хотите, вы действительно хотите просто echo *).
tripleee
@triplee Я вообще не использовал echoкоманду. Я просто использовал его, чтобы продемонстрировать результат, fooкоторый появляется в 3-й строке как результат 2-й строки.
Рон
Но просто basename "${file%.*}"сделал бы то же самое; вы используете подстановку команд для захвата ее выходных данных, только для echoтого же самого вывода немедленно. (Без кавычек результат номинально другой; но это вряд ли уместно, тем более, что особенность здесь.)
tripleee
Также basename "$file" .txtизбегает сложности подстановки параметров.
tripleee
1
@Ron Прочтите его первый комментарий, прежде чем обвинять его в том, что он тратит наше время.
frederick99
14

Я думаю, что если вам просто нужно имя файла, вы можете попробовать это:

FULLPATH=/usr/share/X11/xorg.conf.d/50-synaptics.conf

# Remove all the prefix until the "/" character
FILENAME=${FULLPATH##*/}

# Remove all the prefix until the "." character
FILEEXTENSION=${FILENAME##*.}

# Remove a suffix, in our case, the filename. This will return the name of the directory that contains this file.
BASEDIRECTORY=${FULLPATH%$FILENAME}

echo "path = $FULLPATH"
echo "file name = $FILENAME"
echo "file extension = $FILEEXTENSION"
echo "base directory = $BASEDIRECTORY"

И это все = D.

Эндрю Вулфганг
источник
Просто хотел BASEDIRECTORY :) Спасибо!
Карлос Рикардо
12

Вы можете принудительно вырезать для отображения всех полей и последующих, добавляя -к номеру поля.

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

Так что, если ФАЙЛ eth0.pcap.gz, расширение будетpcap.gz

Используя ту же логику, вы также можете получить имя файла, используя '-' с cut следующим образом:

NAME=`basename "$FILE" | cut -d'.' -f-1`

Это работает даже для имен файлов, которые не имеют никакого расширения.

Maciek Gajewski
источник
8

Волшебное распознавание файлов

В дополнение к множеству хороших ответов на этот вопрос переполнения стека я хотел бы добавить:

В Linux и других unixen существует волшебная команда с именем file, которая определяет тип файла, анализируя некоторые первые байты файла. Это очень старый инструмент, изначально используемый для серверов печати (если не создан для ... Я не уверен в этом).

file myfile.txt
myfile.txt: UTF-8 Unicode text

file -b --mime-type myfile.txt
text/plain

Расширения стандартов можно найти в /etc/mime.types(на моем Debian стола GNU / Linux См. man fileИ man mime.typesВозможно , вам придется установить. fileПолезность и mime-supportпакеты):

grep $( file -b --mime-type myfile.txt ) </etc/mime.types
text/plain      asc txt text pot brf srt

Вы могли бы создать функция для определения правильного расширения. Есть небольшой (не идеальный) образец:

file2ext() {
    local _mimetype=$(file -Lb --mime-type "$1") _line _basemimetype
    case ${_mimetype##*[/.-]} in
        gzip | bzip2 | xz | z )
            _mimetype=${_mimetype##*[/.-]}
            _mimetype=${_mimetype//ip}
            _basemimetype=$(file -zLb --mime-type "$1")
            ;;
        stream )
            _mimetype=($(file -Lb "$1"))
            [ "${_mimetype[1]}" = "compressed" ] &&
                _basemimetype=$(file -b --mime-type - < <(
                        ${_mimetype,,} -d <"$1")) ||
                _basemimetype=${_mimetype,,}
            _mimetype=${_mimetype,,}
            ;;
        executable )  _mimetype='' _basemimetype='' ;;
        dosexec )     _mimetype='' _basemimetype='exe' ;;
        shellscript ) _mimetype='' _basemimetype='sh' ;;
        * )
            _basemimetype=$_mimetype
            _mimetype=''
            ;;
    esac
    while read -a _line ;do
        if [ "$_line" == "$_basemimetype" ] ;then
            [ "$_line[1]" ] &&
                _basemimetype=${_line[1]} ||
                _basemimetype=${_basemimetype##*[/.-]}
            break
        fi
        done </etc/mime.types
    case ${_basemimetype##*[/.-]} in
        executable ) _basemimetype='' ;;
        shellscript ) _basemimetype='sh' ;;
        dosexec ) _basemimetype='exe' ;;
        * ) ;;
    esac
    [ "$_mimetype" ] && [ "$_basemimetype" != "$_mimetype" ] &&
      printf ${2+-v} $2 "%s.%s" ${_basemimetype##*[/.-]} ${_mimetype##*[/.-]} ||
      printf ${2+-v} $2 "%s" ${_basemimetype##*[/.-]}
}

Эта функция может установить переменную Bash, которую можно использовать позже:

(Это вдохновлено правильным ответом @Petesh):

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

echo "$fullfile -> $filename . $extension"
Ф. Хаури
источник
8

Итак, если я правильно понимаю, проблема здесь в том, как получить имя и полное расширение файла, который имеет несколько расширений, например stuff.tar.gz.

Это работает для меня:

fullfile="stuff.tar.gz"
fileExt=${fullfile#*.}
fileName=${fullfile%*.$fileExt}

Это даст вам stuffкак имя файла и .tar.gzкак расширение. Он работает для любого количества расширений, включая 0. Надеюсь, это поможет всем, у кого возникла такая же проблема =)

Al3xXx
источник
Правильный результат (в соответствии с тем os.path.splitext, чего хочет ОП) ('stuff.tar', '.gz').
Cyker
6

Я использую следующий скрипт

$ echo "foo.tar.gz"|rev|cut -d"." -f3-|rev
foo
Джойдип Датта
источник
Это не эффективно вообще. Слишком много разветвляется, что совершенно не нужно, поскольку эту операцию можно выполнить в чистом Bash без необходимости каких-либо внешних команд и разветвления.
Codeforester
5
$ F = "text file.test.txt"  
$ echo ${F/*./}  
txt  

Это обслуживает несколько точек и пробелов в имени файла, однако, если нет расширения, оно возвращает само имя файла. Легко проверить, хотя; просто проверьте, что имя файла и расширение совпадают.

Естественно, этот метод не работает для файлов .tar.gz. Однако это может быть обработано в два этапа. Если расширение - gz, проверьте еще раз, есть ли расширение tar.

Мириам Инглиш
источник
5

Как извлечь имя файла и расширение в рыбе :

function split-filename-extension --description "Prints the filename and extension"
  for file in $argv
    if test -f $file
      set --local extension (echo $file | awk -F. '{print $NF}')
      set --local filename (basename $file .$extension)
      echo "$filename $extension"
    else
      echo "$file is not a valid file"
    end
  end
end

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

Применение:

$ split-filename-extension foo-0.4.2.zip bar.tar.gz
foo-0.4.2 zip  # Looks good!
bar.tar gz  # Careful, you probably want .tar.gz as the extension.

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


Если вы имеете дело с ограниченным набором расширений и знаете их все, попробуйте это:

switch $file
  case *.tar
    echo (basename $file .tar) tar
  case *.tar.bz2
    echo (basename $file .tar.bz2) tar.bz2
  case *.tar.gz
    echo (basename $file .tar.gz) tar.gz
  # and so on
end

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

Деннис
источник
4

Вот код с AWK . Это можно сделать проще. Но я не хорош в AWK.

filename$ ls
abc.a.txt  a.b.c.txt  pp-kk.txt
filename$ find . -type f | awk -F/ '{print $2}' | rev | awk -F"." '{$1="";print}' | rev | awk 'gsub(" ",".") ,sub(".$", "")'
abc.a
a.b.c
pp-kk
filename$ find . -type f | awk -F/ '{print $2}' | awk -F"." '{print $NF}'
txt
txt
txt
smilyface
источник
Вам не нужно первое выражение awk в последнем примере, верно?
BHSPitMonkey
Вы можете избежать передачи Awk в Awk, выполнив другое split(). awk -F / '{ n=split($2, a, "."); print a[n] }' uses / `в качестве разделителя верхнего уровня, но затем разделяет вторые поля .и печатает последний элемент из нового массива.
tripleee
4

Просто использовать ${parameter%word}

В твоем случае:

${FILE%.*}

Если вы хотите проверить это, все последующие работы и просто удалите расширение:

FILE=abc.xyz; echo ${FILE%.*};
FILE=123.abc.xyz; echo ${FILE%.*};
FILE=abc; echo ${FILE%.*};
ENYO
источник
2
Почему отрицательный голос? Это все еще полезно, хотя вокруг =знаков не должно быть пробелов .
Серебряный Волк - Восстановить Монику
1
Это отлично работает. Спасибо! (теперь у него нет пробелов вокруг знаков равенства, если по этой причине он был отклонен)
Алекс. С.
3

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

filename=$(basename ${fullname%.*})
CVR
источник
У меня не сработало: «basename: отсутствующий операнд. Попробуйте« basename --help »для получения дополнительной информации».
helmy
Странно, вы уверены, что используете Bash? В моем случае с обеими версиями 3.2.25 (старый CentOS) и 4.3.30 (Debian Jessie) он работает без нареканий.
CVR
Может быть, в имени файла есть пробел? Попробуйте использоватьfilename="$(basename "${fullname%.*}")"
Адриан
Второй аргумент to basenameявляется необязательным, но указывает расширение для удаления. Подстановка все еще может быть полезной, но, возможно, на basenameсамом деле это не так, поскольку вы можете выполнять все эти подстановки с помощью встроенных команд оболочки.
tripleee
3

Основанный в основном на превосходном @ mklement0 и переполненном случайными, полезными башизмами, а также другими ответами на этот / другие вопросы / "этот чертов интернет" ... Я обернул все это в немного, немного более понятно, многоразовая функция мои (или ваш) , .bash_profileкоторый заботится о том , что (я считаю) должна быть более надежной версией dirname/ basename/ то , что у вас ..

function path { SAVEIFS=$IFS; IFS=""   # stash IFS for safe-keeping, etc.
    [[ $# != 2 ]] && echo "usage: path <path> <dir|name|fullname|ext>" && return    # demand 2 arguments
    [[ $1 =~ ^(.*/)?(.+)?$ ]] && {     # regex parse the path
        dir=${BASH_REMATCH[1]}
        file=${BASH_REMATCH[2]}
        ext=$([[ $file = *.* ]] && printf %s ${file##*.} || printf '')
        # edge cases for extensionless files and files like ".nesh_profile.coffee"
        [[ $file == $ext ]] && fnr=$file && ext='' || fnr=${file:0:$((${#file}-${#ext}))}
        case "$2" in
             dir) echo      "${dir%/*}"; ;;
            name) echo      "${fnr%.*}"; ;;
        fullname) echo "${fnr%.*}.$ext"; ;;
             ext) echo           "$ext"; ;;
        esac
    }
    IFS=$SAVEIFS
}     

Примеры использования ...

SOMEPATH=/path/to.some/.random\ file.gzip
path $SOMEPATH dir        # /path/to.some
path $SOMEPATH name       # .random file
path $SOMEPATH ext        # gzip
path $SOMEPATH fullname   # .random file.gzip                     
path gobbledygook         # usage: -bash <path> <dir|name|fullname|ext>
Алекс Грей
источник
1
Красиво сделано; Несколько советов: - Вы, кажется, совсем не полагаетесь $IFS(и если бы это было так, вы могли бы использовать его localдля локализации). - Лучше использовать localпеременные. - Ваше сообщение об ошибке должно быть выведено stderr, а не stdout(использовать 1>&2), и вы должны вернуть ненулевой код выхода. - Лучше переименовать fullnameв basename(первый предлагает путь с компонентами dir). - nameбезоговорочно добавляет .(точка), даже если оригинал не имеет ни одного. Вы можете просто использовать basenameутилиту, но обратите внимание, что она игнорирует завершение /.
mklement0
2

Простой ответ:

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

tar -zxvf $1
cd ${1%.tar.*}

Это прервет последнее появление .tar. <что - то> .

В целом, если вы хотите удалить последнее вхождение. <что - то> . <что-то еще> тогда

${1.*.*}

должно работать нормально.

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

RandyP
источник
Есть ли способ сделать совпадение без учета регистра?
tonix
2

Если вы также хотите разрешить пустые расширения, это самое короткое, что я могу придумать:

echo 'hello.txt' | sed -r 's/.+\.(.+)|.*/\1/' # EXTENSION
echo 'hello.txt' | sed -r 's/(.+)\..+|(.*)/\1\2/' # FILENAME

1-я строка объяснила: он соответствует PATH.EXT или НИЧЕГО и заменяет его на EXT. Если НИЧЕГО было найдено, группа ext не перехватывается.

phil294
источник
2

Это единственный, который работал на меня:

path='folder/other_folder/file.js'

base=${path##*/}
echo ${base%.*}

>> file

Это также может быть использовано в интерполяции строк, но, к сожалению, вы должны установить baseзаранее.

Кен Мюллер
источник
1

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

#! /bin/bash 

#
# Finds 
# -- name and extension pairs
# -- null extension when there isn't an extension.
# -- Finds name of a hidden file without an extension
# 

declare -a fileNames=(
  '.Montreal' 
  '.Rome.txt' 
  'Loundon.txt' 
  'Paris' 
  'San Diego.txt'
  'San Francisco' 
  )

echo "Script ${0} finding name and extension pairs."
echo 

for theFileName in "${fileNames[@]}"
do
     echo "theFileName=${theFileName}"  

     # Get the proposed name by chopping off the extension
     name="${theFileName%.*}"

     # get extension.  Set to null when there isn't an extension
     # Thanks to mklement0 in a comment above.
     extension=$([[ "$theFileName" == *.* ]] && echo ".${theFileName##*.}" || echo '')

     # a hidden file without extenson?
     if [ "${theFileName}" = "${extension}" ] ; then
         # hidden file without extension.  Fixup.
         name=${theFileName}
         extension=""
     fi

     echo "  name=${name}"
     echo "  extension=${extension}"
done 

Тестовый прогон.

$ config/Name\&Extension.bash 
Script config/Name&Extension.bash finding name and extension pairs.

theFileName=.Montreal
  name=.Montreal
  extension=
theFileName=.Rome.txt
  name=.Rome
  extension=.txt
theFileName=Loundon.txt
  name=Loundon
  extension=.txt
theFileName=Paris
  name=Paris
  extension=
theFileName=San Diego.txt
  name=San Diego
  extension=.txt
theFileName=San Francisco
  name=San Francisco
  extension=
$ 

К вашему сведению: полную программу транслитерации и другие тестовые примеры можно найти здесь: https://www.dropbox.com/s/4c6m0f2e28a1vxf/avoid-clashes-code.zip?dl=0

historystamp
источник
Из всех решений это единственное, которое возвращает пустую строку, когда файл не имеет расширения с:extension=$([[ "$theFileName" == *.* ]] && echo ".${theFileName##*.}" || echo '')
f0nzie
1

Используя пример файла /Users/Jonathan/Scripts/bash/MyScript.sh, этот код:

MY_EXT=".${0##*.}"
ME=$(/usr/bin/basename "${0}" "${MY_EXT}")

приведет к ${ME}тому, чтобы быть MyScriptи ${MY_EXT}быть .sh:


Автор сценария:

#!/bin/bash
set -e

MY_EXT=".${0##*.}"
ME=$(/usr/bin/basename "${0}" "${MY_EXT}")

echo "${ME} - ${MY_EXT}"

Некоторые тесты:

$ ./MyScript.sh 
MyScript - .sh

$ bash MyScript.sh
MyScript - .sh

$ /Users/Jonathan/Scripts/bash/MyScript.sh
MyScript - .sh

$ bash /Users/Jonathan/Scripts/bash/MyScript.sh
MyScript - .sh
Чаун
источник
2
Не уверен, почему это так много отрицательных голосов - это на самом деле более эффективно, чем принятый ответ. (Как последний, он также разрывается с входными именами файлов без расширения). Использование явного пути к basename, возможно, излишне.
mklement0
1

Из ответов выше, самый короткий oneliner для имитации Python

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

Предполагая, что ваш файл действительно имеет расширение,

EXT="${PATH##*.}"; FILE=$(basename "$PATH" .$EXT)
commonpike
источник
У меня есть отрицательные отзывы по этому. Я обдумываю удалить ответ, людям это как-то не нравится.
commonpike
Базовое имя не удаляет расширение, просто путь.
Дэвид Каллен
Прошло так много времени с тех пор, как я заглянул на страницу руководства, и я забыл о опции SUFFIX.
Дэвид Каллен
Вы должны знать, какое расширение вы хотите удалить, прежде чем вы знаете, что вставить, EXTтак что это черепахи до конца. (Кроме того, вам следует избегать использования заглавных букв в именах ваших личных переменных; они зарезервированы для системных переменных.)
tripleee