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

1716

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

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

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

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

Это может быть легко сделано в Python с

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

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

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

  • 0
    Этот вопрос объясняет эту технику bash и несколько других связанных с ней.
  • 22
    Применяя замечательные ответы ниже, не просто вставляйте свою переменную, как показано здесь : Wrong: extension="{$filename##*.}" Как я это делал некоторое время! Переместите $ за пределы фигурных скобок: вправо: extension="${filename##*.}"
Показать ещё 5 комментариев
Теги:
string
filenames

36 ответов

2797
Лучший ответ

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

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

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

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

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

  • 81
    Проверьте gnu.org/software/bash/manual/html_node/… для полного набора функций.
  • 21
    Добавьте кавычки в $ fullfile, иначе вы рискуете сломать имя файла.
Показать ещё 18 комментариев
501
~% FILE="example.tar.gz"
~% echo "${FILE%%.*}"
example
~% echo "${FILE%.*}"
example.tar
~% echo "${FILE#*.}"
tar.gz
~% echo "${FILE##*.}"
gz

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

  • 16
    Вы (возможно, непреднамеренно) поднимаете отличный вопрос о том, что делать, если в части «расширения» имени файла есть 2 точки, как в .tar.gz ... Я никогда не рассматривал эту проблему, и я подозреваю, что это невозможно решить, не зная всех возможных допустимых расширений файлов.
  • 7
    Почему не решаемо? В моем примере следует учитывать, что файл содержит два расширения, а не расширение с двумя точками. Вы обрабатываете оба расширения отдельно.
Показать ещё 14 комментариев
321

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

basename filename .extension

например:

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

и получим

filename
  • 48
    Этот второй аргумент в пользу basename довольно откровенен, ты вроде сэр / мадам :)
  • 9
    А как извлечь расширение, используя эту технику? ;) Ой, подожди! Мы на самом деле не знаем этого заранее.
Показать ещё 4 комментария
128

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

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

#!/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 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  = ""
  • 2
    Вместо dir="${fullpath:0:${#fullpath} - ${#filename}}" я часто видел dir="${fullpath%$filename}" . Проще написать. Не уверен, есть ли какая-либо реальная разница в скорости или ошибки.
  • 2
    При этом используется #! / Bin / bash, что почти всегда неверно. Предпочитайте #! / Bin / sh, если это возможно, или #! / Usr / bin / env bash, если нет.
Показать ещё 9 комментариев
44

Вы можете использовать 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
  • 0
    Томи По опубликовал то же самое за 2 года до этого.
Показать ещё 2 комментария
28
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 заменяет любое количество символов, за которыми следует символ "." в начале строки, без ничего (т.е. удаляет все с начала строки до конечной точки включительно), Это жадная подстановка, которая является действием по умолчанию.

  • 0
    Этот разрыв для файлов без расширения, поскольку он будет печатать то же самое для имени и расширения. Поэтому я использую sed 's,\.[^\.]*$,,' для имени, а sed 's,.*\.,., ;t ;g' для расширения (использует нетипичный test и команды get , наряду с типичной командой substitute ).
27

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

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

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

Выходы:

filename: thisfile
extension: txt
23

Вы можете использовать команду 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 поля после разделителя.)
  • 0
    @ClaytonHughes Вы правы, и я должен был проверить это лучше. Добавлено другое решение.
Показать ещё 2 комментария
21

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

Справочная реализация

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

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

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

Все тесты прошли.

  • 1
    нет, базовое имя файла для text.tar.gz должно быть text а расширение должно быть .tar.gz
  • 0
    @ frederick99 Как я уже сказал, решение здесь соответствует реализации os.path.splitext в Python. Является ли эта реализация вменяемой для возможных противоречивых мнений - это еще одна тема.
21

Вот несколько альтернативных предложений (в основном в 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 '^[/]\|^~/'

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

14

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

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

[Пересмотрено от однострочного к общей функции bash, поведение теперь совместимо с утилитами dirname и basename; Обоснование добавлено.]

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

  • Для имен файлов без расширения (называемых суффиксами в оставшейся части ответа) 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'

Обратите внимание, что аргументы после входного пути свободно выбираются, имена позиционных переменных.
Чтобы пропустить переменные, не представляющие интерес, которые приходят перед теми, которые есть, укажите _ (использовать переменную throw-away $_) или ''; например, для извлечения корня и расширения имени файла, используйте 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

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

  • имя файла без суффикса
  • имя файла, начинающееся с . (не считается началом суффикса)
  • путь ввода, заканчивающийся на / (trailing / игнорируется)
  • входной путь, только имя файла (. возвращается как родительский путь)
  • имя файла, у которого есть более чем . -prefixed токен (только последний считается суффиксом):
----- /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
12

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

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.

  • 0
    Просто хотел BASEDIRECTORY :) Спасибо!
10

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

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

Итак, если FILE - eth0.pcap.gz, EXTENSION будет pcap.gz

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

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

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

7

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

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

В 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"
7

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

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

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

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

  • 0
    Правильный результат (в соответствии с os.path.splitext , который является тем, что хочет OP): ('stuff.tar', '.gz') .
6

Я использую следующие script

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

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

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

  • 0
    Проголосовал, но это не работает после того, как я попробовал.
4

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

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
  • 0
    Вам не нужно первое выражение awk в последнем примере, верно?
  • 0
    Вы можете избежать передачи Awk в Awk, выполнив еще один split() . awk -F / '{ n=split($2, a, "."); print a[n] }' uses / `в качестве разделителя верхнего уровня, но затем разбивает вторые поля на . и печатает последний элемент из нового массива.
2

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

filename=$(basename ${fullname%.*})
  • 0
    У меня не сработало: «basename: отсутствующий операнд. Попробуйте« basename --help »для получения дополнительной информации».
  • 0
    Странно, вы уверены, что используете Bash? В моем случае с обеими версиями 3.2.25 (старый CentOS) и 4.3.30 (Debian Jessie) он работает безупречно.
Показать ещё 2 комментария
2

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

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

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

Это отключит последнее вхождение .tar. <something> .

В более общем плане, если вы хотите удалить последнее вхождение. <something> . < something-else > затем

${1.*.*}

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

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

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

Просто используйте ${parameter%word}

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

${FILE%.*}

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

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

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

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

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

1

Из приведенных выше ответов, кратчайший oneliner, чтобы имитировать Python

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

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

EXT="${PATH##*.}"; FILE=$(basename "$PATH" .$EXT)
  • 0
    У меня есть отрицательные отзывы по этому. Я обдумываю удалить ответ, людям это как-то не нравится.
  • 0
    Базовое имя не удаляет расширение, просто путь.
Показать ещё 4 комментария
1

Основанный в основном вне @mklement0 превосходный и наполненный случайными, полезными bashisms - так же как другие ответы на этот/другие вопросы / ", что darn интернет"... Я обернул это все немного, немного более понятной, многоразовой функции для моего (или вашего) .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 extesionless 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 в basename (первое предлагает путь с компонентами dir). - name безоговорочно добавляет . (точка), даже если оригинал не имел ни одного. Вы можете просто использовать утилиту basename , но учтите, что она игнорирует завершающий / .
0

ИМХО, лучшее решение уже было дано (с использованием расширения параметров оболочки) и является лучшим на данный момент.

Однако я добавляю эту, которая просто использует команды dumbs, которая неэффективна и которую никто никогда не должен использовать серьезно:

FILENAME=$(echo $FILE | cut -d . -f 1-$(printf $FILE | tr . '\n' | wc -l))
EXTENSION=$(echo $FILE | tr . '\n' | tail -1)

Добавлено просто для удовольствия :-)

0

Вот решение sed, которое извлекает компоненты пути в различных формах и может обрабатывать большинство случаев:

## Enter the input path and field separator character, for example:
## (separatorChar must not be present in inputPath)

inputPath="/path/to/Foo.bar"
separatorChar=":"

## sed extracts the path components and assigns them to output variables

oldIFS="$IFS"
IFS="$separatorChar"
read dirPathWithSlash dirPath fileNameWithExt fileName fileExtWithDot fileExt <<<"$(sed -En '
s/^[[:space:]]+//
s/[[:space:]]+$//
t l1
:l1
s/^([^/]|$)//
t
s/[/]+$//
t l2
:l2
s/^$/filesystem\/\
filesystem/p
t
h
s/^(.*)([/])([^/]+)$/\1\2\
\1\
\3/p
g
t l3
:l3
s/^.*[/]([^/]+)([.])([a-zA-Z0-9]+)$/\1\
\2\3\
\3/p
t
s/^.*[/](.+)$/\1/p
' <<<"$inputPath" | tr "\n" "$separatorChar")"
IFS="$oldIFS"

## Results (all use separatorChar=":")

## inputPath        = /path/to/Foo.bar
## dirPathWithSlash = /path/to/
## dirPath          = /path/to 
## fileNameWithExt  = Foo.bar
## fileName         = Foo
## fileExtWithDot   = .bar
## fileExt          = bar

## inputPath        = /path/to/Foobar
## dirPathWithSlash = /path/to/
## dirPath          = /path/to 
## fileNameWithExt  = Foobar
## fileName         = Foobar
## fileExtWithDot   =
## fileExt          =

## inputPath        = /path/to/...bar
## dirPathWithSlash = /path/to/
## dirPath          = /path/to 
## fileNameWithExt  = ...bar
## fileName         = ..
## fileExtWithDot   = .bar
## fileExt          = bar

## inputPath        = /path/to/..bar
## dirPathWithSlash = /path/to/
## dirPath          = /path/to 
## fileNameWithExt  = ..bar
## fileName         = .
## fileExtWithDot   = .bar
## fileExt          = bar

## inputPath        = /path/to/.bar
## dirPathWithSlash = /path/to/
## dirPath          = /path/to 
## fileNameWithExt  = .bar
## fileName         = .bar
## fileExtWithDot   = 
## fileExt          = 

## inputPath        = /path/to/...
## dirPathWithSlash = /path/to/
## dirPath          = /path/to 
## fileNameWithExt  = ...
## fileName         = ...
## fileExtWithDot   =
## fileExt          =

## inputPath        = /path/to/Foo.
## dirPathWithSlash = /path/to/
## dirPath          = /path/to 
## fileNameWithExt  = Foo.
## fileName         = Foo.
## fileExtWithDot   =
## fileExt          =

## inputPath        = / (the root directory)
## dirPathWithSlash = filesystem/
## dirPath          = filesystem
## fileNameWithExt  =
## fileName         =
## fileExtWithDot   =
## fileExt          =

## inputPath        =  (invalid because empty)
## dirPathWithSlash =
## dirPath          =
## fileNameWithExt  =
## fileName         =
## fileExtWithDot   =
## fileExt          =

## inputPath        = Foo/bar (invalid because doesn't start with a forward slash)
## dirPathWithSlash =
## dirPath          =
## fileNameWithExt  =
## fileName         =
## fileExtWithDot   =
## fileExt          =

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

sed анализирует входной путь и печатает следующие компоненты пути в порядке по отдельным строкам:

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

tr преобразует вывод sed в строку с разделителями символов разделителя с указанными выше компонентами пути.

read использует разделительный символ как разделитель полей (IFS = "$ separatorChar" ) и присваивает каждому из компонентов пути свою соответствующую переменную.

Здесь работает конструктор sed:

  • s/^ [[: space:]] +// и s/[[: space:]] + $// листинг любых ведущих и/или завершающие пробельные символы
  • t l1 и : l1 обновляет функцию t для следующей функции s
  • s/^ ([^/] | $)// и t тесты для недопустимого пути ввода (который не начинается с косой черты) в этом случае он оставляет все выходные строки пустыми и завершает команду sed
  • s/[/] + $// удаляет любые завершающие слэши
  • t l2 и : l2 обновляет функцию t для следующей s-функции
  • s/^ $/filesystem/\ [новая строка] файловая система /p и t тесты для специального случая, когда входной путь состоит из корневого каталога /, в в этом случае он выводит файловую систему / и файловую систему для строк dirPathWithSlash и dirPath, оставляет все остальные выходные строки пустыми, и завершает команду sed
  • h сохраняет входной путь в пространстве удержания
  • s/^ (. *) ([/]) ([^/] +) $/\ 1\2\[newline]\1\[newline]\3/p печатает строки dirPathWithSlash, dirPath и fileNameWithExt
  • g извлекает входной путь из пространства удержания
  • t l3 и : l3 обновляет функцию t для следующей s-функции
  • s/^.* [/] ([^/] +) ([.]) ([A-Za-Z0-9] +) $/\ 1\[перевод строки]\2\3\[newline]\3/p и t печатает имя_файла, fileExtWithDot и fileExt вывода для случая, когда существует расширение файла (предполагается, что оно состоит только из буквенно-цифровых символов), а затем завершает команду sed
  • s/^.* [/] (. +) $/\ 1/p выводит имя_файла, но не fileExtWithDot и fileExt для случая, когда расширение файла не существует, а затем завершает команду sed
0

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

#! /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=
$ 

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

0

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

sed 's/^/./' | rev | cut -d. -f2- | rev | cut -c2-

чтобы получить имя файла и

sed 's/^/./' | rev | cut -d. -f1  | rev

чтобы получить расширение.

Тестовый пример:

echo "filename.gz"     | sed 's/^/./' | rev | cut -d. -f2- | rev | cut -c2-
echo "filename.gz"     | sed 's/^/./' | rev | cut -d. -f1  | rev
echo "filename"        | sed 's/^/./' | rev | cut -d. -f2- | rev | cut -c2-
echo "filename"        | sed 's/^/./' | rev | cut -d. -f1  | rev
echo "filename.tar.gz" | sed 's/^/./' | rev | cut -d. -f2- | rev | cut -c2-
echo "filename.tar.gz" | sed 's/^/./' | rev | cut -d. -f1  | rev
0

Чтобы сделать dir более полезным (в случае, если локальный файл без пути указан как ввод), я сделал следующее:

# Substring from 0 thru pos of filename
dir="${fullpath:0:${#fullpath} - ${#filename}}"
if [[ -z "$dir" ]]; then
    dir="./"
fi

Это позволяет вам сделать что-то полезное, например, добавить суффикс в базовое имя входного файла:

outfile=${dir}${base}_suffix.${ext}

testcase: foo.bar
dir: "./"
base: "foo"
ext: "bar"
outfile: "./foo_suffix.bar"

testcase: /home/me/foo.bar
dir: "/home/me/"
base: "foo"
ext: "bar"
outfile: "/home/me/foo_suffix.bar"
0

Возможно, в tar есть опция; ты проверил человека? В противном случае вы можете использовать Bash расширение строки:

test="mpc-1.0.1.tar.gz"
noExt="${test/.tar.gz/}" # Remove the string '.tar.gz'
echo $noExt
  • 0
    cd $ (tar tf $ 1 | sed -n 1p)
0

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

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

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


Script:

#!/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 , возможно, излишне.
-1

Вы также можете использовать цикл for и tr для извлечения имени файла из пути...

for x in `echo $path | tr "/" " "`; do filename=$x; done

Tr заменяет все/разделители в пути пробелами, поэтому делает список строк, а цикл for проверяет их, оставляя последнее в переменной имени файла.

  • 3
    Если вы собираетесь сделать это таким образом, сохраните себе несколько вилок и вместо этого используйте: (IFS=/ ; for x in $path; do filename=$x; done) . (...) подоболочка необходима для локализации назначения в IFS.
-2

Простой bash один вкладыш. Я использовал это, чтобы удалить расширение rst из всех файлов в pwd

for each in `ls -1 *.rst`
do
     a=$(echo $each | wc -c)
     echo $each | cut -c -$(( $a-5 )) >> blognames
done

Что он делает?

1) ls -1 *.rst отобразит все файлы на stdout в новой строке (попробуйте).

2) echo $each | wc -c подсчитывает количество символов в каждом имени файла.

3) echo $each | cut -c -$(( $a-5 )) выбирает до 4 символов, т.е. .rst.

Ещё вопросы

Сообщество Overcoder
Наверх
Меню