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

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

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

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

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

38 ответов

Решение

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

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

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

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

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

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

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

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

basename filename .extension

например:

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

и мы получаем

filename

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

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

#!/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 = ""
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 заменяет любое количество символов, за которыми следует "." символ в начале строки, с ничем (т.е. он удаляет все от начала строки до конечной точки включительно). Это жадная замена, которая является действием по умолчанию.

Ты можешь использовать basename,

Пример:

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

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

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

tar -zxvf $1
cd $(basename $1 .tar.gz)

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

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

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

Выходы:

filename: thisfile
extension: txt

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

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

Не нужно беспокоиться с 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' |
|---------------|-----------|-------|

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

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

[Пересмотрен с однострочной на общую функцию 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'

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

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

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

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

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

$ file=/blaabla/bla/blah/foo.txt
echo $(basename ${file%.*}) # foo

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

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.

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

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

Так что, если ФАЙЛ eth0.pcap.gzРАСШИРЕНИЕ будет pcap.gz

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

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

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

$ F = "text file.test.txt"  
$ echo ${F/*./}  
txt  

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

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

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

path='folder/other_folder/file.js'

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

>> file

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

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

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

Под 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

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

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"

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

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

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

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

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

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

${FILE%.*}

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

FILE=abc.xyz; echo ${FILE%.*};
FILE=123.abc.xyz; echo ${FILE%.*};
FILE=abc; echo ${FILE%.*};

В предыдущем ответе не использовалось регулярное выражение bash
. Вот чистое решение ERE для bash, которое разбивает путь на:

  • Путь к каталогу с его окончанием, если оно присутствует
    . Регулярное выражение, которое отбрасывает замыкание, настолько длиннее, что я его не публиковал.
  • Имя файла , исключая (последнее) точечное расширение
  • (Последнее) точечное расширение с ведущим

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

      #!/bin/bash

for path; do

####### the relevant part ######

[[ $path =~ ^(\.{1,2}|.*/\.{0,2})$|^(.*/)([^/]+)(\.[^/]*)$|^(.*/)(.+)$|^(.+)(\..*)$|^(.+)$ ]]

dirpath="${BASH_REMATCH[1]}${BASH_REMATCH[2]}${BASH_REMATCH[5]}"
filename="${BASH_REMATCH[3]}${BASH_REMATCH[6]}${BASH_REMATCH[7]}${BASH_REMATCH[9]}"
filext="${BASH_REMATCH[4]}${BASH_REMATCH[8]}"

# dirpath should be non-null
[[ $dirpath ]] || dirpath='.'

################################

printf '%s=%q\n' \
    path     "$path" \
    dirpath  "$dirpath" \
    filename "$filename" \
    filext   "$filext"

done

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

По сути, это гарантирует, что только одно подвыражение (разделенное в регулярном выражении) может захватить ввод. Благодаря этому вы можете объединить все группы захвата одного типа (например, связанные с путем к каталогу), хранящиеся в не беспокойтесь, потому что самое большее одно из них будет ненулевым.

Вот результаты расширенного, но не исчерпывающего набора примеров:
      +--------------------------------------------------------+
| input             dirpath        filename       filext |
+--------------------------------------------------------+
''                  .              ''             ''
.                   .              ''             ''
..                  ..             ''             ''
...                 .              ..             .
.file               .              .file          ''
.file.              .              .file          .
.file..             .              .file.         .
.file.Z             .              .file          .Z
.file.sh.Z          .              .file.sh       .Z
file                .              file           ''
file.               .              file           .
file..              .              file.          .
file.Z              .              file           .Z
file.sh.Z           .              file.sh        .Z
dir/                dir/           ''             ''
dir/.               dir/.          ''             ''
dir/...             dir/           ..             .
dir/.file           dir/           .file          ''
dir/.file.          dir/           .file          .
dir/.file..         dir/           .file.         .
dir/.file.Z         dir/           .file          .Z
dir/.file.x.Z       dir/           .file.x        .Z
dir/file            dir/           file           ''
dir/file.           dir/           file           .
dir/file..          dir/           file.          .
dir/file.Z          dir/           file           .Z
dir/file.x.Z        dir/           file.x         .Z
dir./.              dir./.         ''             ''
dir./...            dir./          ..             .
dir./.file          dir./          .file          ''
dir./.file.         dir./          .file          .
dir./.file..        dir./          .file.         .
dir./.file.Z        dir./          .file          .Z
dir./.file.sh.Z     dir./          .file.sh       .Z
dir./file           dir./          file           ''
dir./file.          dir./          file           .
dir./file..         dir./          file.          .
dir./file.Z         dir./          file           .Z
dir./file.x.Z       dir./          file.x         .Z
dir//               dir//          ''             ''
dir//.              dir//.         ''             ''
dir//...            dir//          ..             .
dir//.file          dir//          .file          ''
dir//.file.         dir//          .file          .
dir//.file..        dir//          .file.         .
dir//.file.Z        dir//          .file          .Z
dir//.file.x.Z      dir//          .file.x        .Z
dir//file           dir//          file           ''
dir//file.          dir//          file           .
dir//file..         dir//          file.          .
dir//file.Z         dir//          file           .Z
dir//file.x.Z       dir//          file.x         .Z
dir.//.             dir.//.        ''             ''
dir.//...           dir.//         ..             .
dir.//.file         dir.//         .file          ''
dir.//.file.        dir.//         .file          .
dir.//.file..       dir.//         .file.         .
dir.//.file.Z       dir.//         .file          .Z
dir.//.file.x.Z     dir.//         .file.x        .Z
dir.//file          dir.//         file           ''
dir.//file.         dir.//         file           .
dir.//file..        dir.//         file.          .
dir.//file.Z        dir.//         file           .Z
dir.//file.x.Z      dir.//         file.x         .Z
/                   /              ''             ''
/.                  /.             ''             ''
/..                 /..            ''             ''
/...                /              ..             .
/.file              /              .file          ''
/.file.             /              .file          .
/.file..            /              .file.         .
/.file.Z            /              .file          .Z
/.file.sh.Z         /              .file.sh       .Z
/file               /              file           ''
/file.              /              file           .
/file..             /              file.          .
/file.Z             /              file           .Z
/file.sh.Z          /              file.sh        .Z
/dir/               /dir/          ''             ''
/dir/.              /dir/.         ''             ''
/dir/...            /dir/          ..             .
/dir/.file          /dir/          .file          ''
/dir/.file.         /dir/          .file          .
/dir/.file..        /dir/          .file.         .
/dir/.file.Z        /dir/          .file          .Z
/dir/.file.x.Z      /dir/          .file.x        .Z
/dir/file           /dir/          file           ''
/dir/file.          /dir/          file           .
/dir/file..         /dir/          file.          .
/dir/file.Z         /dir/          file           .Z
/dir/file.x.Z       /dir/          file.x         .Z
/dir./.             /dir./.        ''             ''
/dir./...           /dir./         ..             .
/dir./.file         /dir./         .file          ''
/dir./.file.        /dir./         .file          .
/dir./.file..       /dir./         .file.         .
/dir./.file.Z       /dir./         .file          .Z
/dir./.file.sh.Z    /dir./         .file.sh       .Z
/dir./file          /dir./         file           ''
/dir./file.         /dir./         file           .
/dir./file..        /dir./         file.          .
/dir./file.Z        /dir./         file           .Z
/dir./file.x.Z      /dir./         file.x         .Z
/dir//              /dir//         ''             ''
/dir//.             /dir//.        ''             ''
/dir//...           /dir//         ..             .
/dir//.file         /dir//         .file          ''
/dir//.file.        /dir//         .file          .
/dir//.file..       /dir//         .file.         .
/dir//.file.Z       /dir//         .file          .Z
/dir//.file.x.Z     /dir//         .file.x        .Z
/dir//file          /dir//         file           ''
/dir//file.         /dir//         file           .
/dir//file..        /dir//         file.          .
/dir//file.Z        /dir//         file           .Z
/dir//file.x.Z      /dir//         file.x         .Z
/dir.//.            /dir.//.       ''             ''
/dir.//...          /dir.//        ..             .
/dir.//.file        /dir.//        .file          ''
/dir.//.file.       /dir.//        .file          .
/dir.//.file..      /dir.//        .file.         .
/dir.//.file.Z      /dir.//        .file          .Z
/dir.//.file.x.Z    /dir.//        .file.x        .Z
/dir.//file         /dir.//        file           ''
/dir.//file.        /dir.//        file           .
/dir.//file..       /dir.//        file.          .
/dir.//file.Z       /dir.//        file           .Z
/dir.//file.x.Z     /dir.//        file.x         .Z
//                  //             ''             ''
//.                 //.            ''             ''
//..                //..           ''             ''
//...               //             ..             .
//.file             //             .file          ''
//.file.            //             .file          .
//.file..           //             .file.         .
//.file.Z           //             .file          .Z
//.file.sh.Z        //             .file.sh       .Z
//file              //             file           ''
//file.             //             file           .
//file..            //             file.          .
//file.Z            //             file           .Z
//file.sh.Z         //             file.sh        .Z
//dir/              //dir/         ''             ''
//dir/.             //dir/.        ''             ''
//dir/...           //dir/         ..             .
//dir/.file         //dir/         .file          ''
//dir/.file.        //dir/         .file          .
//dir/.file..       //dir/         .file.         .
//dir/.file.Z       //dir/         .file          .Z
//dir/.file.x.Z     //dir/         .file.x        .Z
//dir/file          //dir/         file           ''
//dir/file.         //dir/         file           .
//dir/file..        //dir/         file.          .
//dir/file.Z        //dir/         file           .Z
//dir/file.x.Z      //dir/         file.x         .Z
//dir./.            //dir./.       ''             ''
//dir./...          //dir./        ..             .
//dir./.file        //dir./        .file          ''
//dir./.file.       //dir./        .file          .
//dir./.file..      //dir./        .file.         .
//dir./.file.Z      //dir./        .file          .Z
//dir./.file.sh.Z   //dir./        .file.sh       .Z
//dir./file         //dir./        file           ''
//dir./file.        //dir./        file           .
//dir./file..       //dir./        file.          .
//dir./file.Z       //dir./        file           .Z
//dir./file.x.Z     //dir./        file.x         .Z
//dir//             //dir//        ''             ''
//dir//.            //dir//.       ''             ''
//dir//...          //dir//        ..             .
//dir//.file        //dir//        .file          ''
//dir//.file.       //dir//        .file          .
//dir//.file..      //dir//        .file.         .
//dir//.file.Z      //dir//        .file          .Z
//dir//.file.x.Z    //dir//        .file.x        .Z
//dir//file         //dir//        file           ''
//dir//file.        //dir//        file           .
//dir//file..       //dir//        file.          .
//dir//file.Z       //dir//        file           .Z
//dir//file.x.Z     //dir//        file.x         .Z
//dir.//.           //dir.//.      ''             ''
//dir.//...         //dir.//       ..             .
//dir.//.file       //dir.//       .file          ''
//dir.//.file.      //dir.//       .file          .
//dir.//.file..     //dir.//       .file.         .
//dir.//.file.Z     //dir.//       .file          .Z
//dir.//.file.x.Z   //dir.//       .file.x        .Z
//dir.//file        //dir.//       file           ''
//dir.//file.       //dir.//       file           .
//dir.//file..      //dir.//       file.          .
//dir.//file.Z      //dir.//       file           .Z
//dir.//file.x.Z    //dir.//       file.x         .Z

Как видите, поведение отличается от а также . Например выходы в то время как регулярное выражение даст вам пустое имя файла для него. То же самое для и , они считаются каталогами, а не именами файлов.

Я рассчитал время с 10000 путями по 256 символов, и это заняло около 1 секунды, в то время как эквивалентное решение оболочки POSIX работает в 2 раза медленнее, а решения, основанные на диких разветвлениях (внешние вызовы внутри loop) как минимум в 60 раз медленнее.

примечание: нет необходимости тестировать пути, содержащие или другие печально известные символы, потому что все символы обрабатываются механизмом регулярных выражений bash одинаково. Единственные персонажи, которые могли бы сломать текущую логику, это и , смешанные или умноженные неожиданным образом. Когда я впервые опубликовал свой ответ, я обнаружил несколько пограничных случаев, которые мне пришлось исправить; Я не могу сказать, что регулярное выражение на 100% пуленепробиваемо, но теперь оно должно быть достаточно надежным.


Кроме того, вот чистое решение оболочки POSIX, которое дает тот же результат:

      #!/bin/sh

for path; do

####### the relevant part ######

basename=${path##*/}

case $basename in
. | ..)
    dirpath="$path"
    filename=''
    filext=''
    basename=''
    ;;
*)
    dirpath=${path%"$basename"}
    filename=${basename#.}
    filename="${basename%"$filename"}${filename%.*}"
    filext=${basename#"$filename"}
esac

# dirpath should be non-null
[ -z "$dirpath" ] && dirpath='.'

################################

printf '%s=%s\n' \
    path     "$filepath" \
    dirpath  "$dirpath" \
    filename "$filename" \
    filext   "$filext"

done

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

  • Особый случай точечных файлов : причина в том, что точечные файлы это концепция UNIX.

  • Особый случай а также : ИМХО кажется очевидным рассматривать их как каталоги, но большинство библиотек этого не делают и заставляют пользователя постобрабатывать результат.

  • Нет поддержки двойных расширений: это потому, что вам потребуется целая база данных для хранения всех действительных двойных расширений, и, прежде всего, потому что расширение файла ничего не значит в UNIX; например, вы можете вызвать tar-архив и это совершенно нормально, вы сможете без каких-либо проблем.

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

$ echo "foo.tar.gz"|rev|cut -d"." -f3-|rev
foo

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

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

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

Вот код с 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

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

filename=$(basename ${fullname%.*})

Основанный в основном на превосходном @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 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>

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

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

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

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

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

${1.*.*}

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

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

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

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

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

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

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

FILENAME=$(echo $FILE | cut -d . -f 1-$(printf $FILE | tr . '\n' | wc -l))
EXTENSION=$(echo $FILE | tr . '\n' | tail -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

Другие вопросы по тегам