Витягнути ім'я файлу та розширення в Bash


2105

Я хочу отримати ім’я файлу (без розширення) та розширення окремо.

Найкраще рішення, яке я знайшов поки що:

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 саме для цього, якщо це можливо.

Якісь кращі ідеї?


Це питання пояснює цю техніку баш та декілька інших пов'язаних з ними.
jjclarkson

28
Застосовуючи чудові відповіді нижче, не просто вставляйте змінну, як я показую тут Неправильно: extension="{$filename##*.}" як я це робив на деякий час! Перемістіть $зовнішні фігурки: Праворуч: extension="${filename##*.}"
Кріс К

4
Це, очевидно, нетривіальна проблема, і для мене важко сказати, чи відповіді, наведені нижче, цілком правильні. Дивно, що це не вбудована операція в (ba) sh (відповіді, здається, реалізують функцію, використовуючи відповідність шаблону). Я вирішив os.path.splitextзамість цього використати Python ...
Пітер Гібсон

1
Оскільки розширення має представляти природу файлу, існує магічна команда, яка перевіряє файл, щоб визначити його природу та запропонувати стандартне розширення . дивіться мою відповідь
Ф. Хаурі

2
Питання в першу чергу проблематичне, тому що .. З точки зору ОС та unix файлових систем загалом, такого розширення файлу немає. Використання "." відокремити частини - це людська конвенція , яка працює лише до тих пір, поки люди згодні її дотримуватися. Наприклад, з програмою 'tar' можна було вирішити назвати вихідні файли "tar". префікс замість суфікса ".tar" - надання "tar.somedir" замість "somedir.tar". Через це не існує рішення "загальне, завжди працює" - ви повинні написати код, який відповідає вашим конкретним потребам та очікуваним назви файлів.
CM

Відповіді:


3499

Спочатку отримайте ім'я файлу без шляху:

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

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

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

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


85
Перевірте gnu.org/software/bash/manual/html_node/… для повного набору функцій.
Д.Шоулі

24
Додайте кілька лапок до "$ fullfile", інакше ви ризикуєте зламати ім'я файлу.
lhunath

47
Чорт забирай, ви могли б навіть написати ім'я файлу = "$ {FullFile ## * /}" і не викликати додатковийbasename
ephemient

45
Це "рішення" не працює, якщо файл не має розширення - натомість виводиться ціле ім'я файлу, що дуже погано, враховуючи, що файли без розширень є всюдисущими.
nccc

43
Фікс для роботи з іменами файлів без розширення: extension=$([[ "$filename" = *.* ]] && echo ".${filename##*.}" || echo ''). Зверніть увагу , що якщо розширення є присутній, то він буде повернутий у тому числі початкового ., наприклад, .txt.
mklement0

683
~% FILE="example.tar.gz"

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

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

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

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

Докладніше див. Розширення параметрів оболонки в посібнику Bash.


22
Ви (можливо, ненавмисно) поставили відмінне запитання про те, що робити, якщо частина "розширення" назви файлу має дві крапки, як у .tar.gz ... Я ніколи не розглядав цю проблему, і підозрюю, що це не вирішується, не знаючи всіх можливих розширень файлу вперед.
rmeador

8
Чому б не вирішити? У моєму прикладі слід врахувати, що файл містить два розширення, а не розширення з двома крапками. Ви обробляєте обидва розширення окремо.
Джуліано

22
Це нерозв’язується на лексичній основі, вам потрібно перевірити тип файлу. Подумайте, чи була у вас гра з назвою, dinosaurs.in.tarі ви її dinosaurs.in.tar.gz
запустили

11
Це ускладнюється, якщо ви проїжджаєте повними стежками. У одного з моїх було "." в каталозі в середині шляху, але жоден у назві файлу. Приклад "a / bc / d / e / ім'я файлу" завив би ".c / d / e / ім'я файлу"
Walt Sellers

6
Очевидно x.tar.gz, що розширення немає, gzа ім'я файлу - x.tarце все. Немає такого подвійного розширення. Я впевнений, що boost :: файлова система обробляє це саме так. (розділений шлях, change_extension ...) та його поведінка заснована на python, якщо я не помиляюся.
v.oddou

430

Зазвичай ви вже знаєте розширення, тому можете скористатися:

basename filename .extension

наприклад:

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

і ми отримуємо

filename

60
Цей другий аргумент basenameє цілком
очевидним

10
А як витягнути розширення, використовуючи цю техніку? ;) Зачекайте! Ми насправді цього не знаємо наперед.
Томаш Гандор

3
Скажімо, у вас є каталог із блискавкою, який закінчується .zipабо .ZIP. Чи є спосіб, як ти міг зробити щось подібне basename $file {.zip,.ZIP}?
Денніс

8
Хоча це відповідає лише на частину питання щодо ОП, воно відповідає на питання, яке я набрав у google. :-) Дуже струнка!
sudo make install

1
легкий і сумісний з POSIX
gpanda

146

Ви можете використовувати магію розширення параметра 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.
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 / додому / мені / ...
/:
    dir = "/"
    base = ""
    ext = ""
/ додому / я /:
    dir = "/ додому / мене /"
    base = ""
    ext = ""
/ головна / я / файл:
    dir = "/ додому / мене /"
    base = "файл"
    ext = ""
/home/me/file.tar:
    dir = "/ додому / мене /"
    base = "файл"
    ext = "смола"
/home/me/file.tar.gz:
    dir = "/ додому / мене /"
    base = "file.tar"
    ext = "gz"
/home/me/.hidden:
    dir = "/ додому / мене /"
    base = ".hidden"
    ext = ""
/home/me/.hidden.tar:
    dir = "/ додому / мене /"
    base = ".hidden"
    ext = "смола"
/ додому / мене / ..:
    dir = "/ додому / мене /"
    base = ".."
    ext = ""
.:
    dir = ""
    base = "."
    ext = ""

2
Замість цього dir="${fullpath:0:${#fullpath} - ${#filename}}"я часто бачив dir="${fullpath%$filename}". Простіше писати. Не впевнений, чи є якась реальна різниця швидкостей або готча.
сумнівним

2
Для цього використовується #! / Bin / bash, що майже завжди неправильно. Віддайте перевагу #! / Bin / sh, якщо можливо, або #! / Usr / bin / env bash, якщо ні.
Хороша людина

@Good Person: Я не знаю, як це майже завжди неправильно: which bash-> /bin/bash; можливо, це ваш дистрибутив?
vol7ron

2
@ vol7ron - на багатьох дистрибутивах bash знаходиться в / usr / local / bin / bash. На OSX багато людей встановлюють оновлений bash в / opt / local / bin / bash. Оскільки такий / bin / bash невірний, і для його використання слід використовувати env. Ще краще використовувати конструкції / bin / sh та POSIX. За винятком Solaris, це оболонка POSIX.
Good Person

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

Привіт Блаухірн, уау, це старі питання. Я думаю, що з побаченнями щось сталося. Я виразно пам'ятаю, як відповів на це питання незабаром після того, як його задали, і там, де лише пара інших відповідей. Чи може бути, що питання було об'єднане з іншим, чи це так?
Bjarke Freund-Hansen

Так, я правильно пам'ятаю. Я спочатку відповідав на це запитання stackoverflow.com/questions/14703318/… того ж дня, коли його запитали, через 2 роки його було об'єднано в це. Мене навряд чи можна звинуватити у повторній відповіді, коли моя відповідь була перенесена таким чином.
Bjarke Freund-Hansen

37
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підміняє будь-яку кількість символів, за якими слідує "."символ на початку рядка, без нічого (тобто він видаляє все від початку рядка до кінцевої крапки, включно). Це жадібна заміна, яка є дією за замовчуванням.


Ця перерва для файлів без розширення, оскільки вона буде друкувати однаково для імені та розширення. Тому я використовую як sed 's,\.[^\.]*$,,'для імені, так і sed 's,.*\.,., ;t ;g'для розширення (використовує нетипові testта getкоманди разом із типовою substituteкомандою).
hIpPy

32

Меллен пише в коментарі до публікації в блозі:

Використовуючи Bash, можна також ${file%.*}отримати ім'я файлу без розширення та ${file##*.}отримати розширення самостійно. Це є,

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

Виходи:

filename: thisfile
extension: txt


29

Немає необхідності турбуватися awkабо sedнавіть perlдля цієї простої задачі. Існує суто os.path.splitext()сумісне рішення Bash, яке використовує лише розширення параметрів.

Довідкова реалізація

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

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

Код Python:

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

Реалізація Баша

Вшанування провідних періодів

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' |
|---------------|-----------|-------|

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

Усі тести пройшли.


2
ні, ім'ям базового файлу для text.tar.gzмає бути, textа розширення.tar.gz
frederick99

2
@ frederick99 Як я вже сказав, рішення тут відповідає реалізації os.path.splitextв Python. Чи справді реалізована реалізація можливих суперечливих даних - це інша тема.
Кікер

Як працюють лапки в рамках шаблону ( "$root")? Що може статися, якщо вони були пропущені? (Я не зміг знайти жодної документації з цього питання.) Також як це обробляє імена файлів з ними *чи ?в них?
ymett

Добре, тестування показує мені , що котирування роблять картину буквальною, тобто *і ?не є спеціальними. Тож дві частини мого запитання відповідають між собою. Я правда, це не документально? Або це слід розуміти з того, що котирування взагалі відключають глобальне розширення?
ymett

Блискуча відповідь! Я просто запропоную трохи простіший варіант для обчислення кореня: root="${path#?}";root="${path::1}${root%.*}"- тоді продовжуйте те ж саме, щоб отримати розширення.
Майлан

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" | вирізати -d '.' --комплікація -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" | вирізати -d '.' -f2- БЕЗ - доповнення отримає другий розділений елемент до кінця рядка $ echo "foo.tar.gz" | вирізати -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 '^[/]\|^~/'

У всіх випадках використання використовується вихідний повний шлях як вхідний, не залежно від проміжних результатів.


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

19

Найменше і найпростіше рішення (в одному рядку):

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

Це марне використанняecho . Взагалі, echo $(command)краще писати просто, commandякщо ви спеціально не вимагаєте, щоб оболонка виконувала токенізацію пробілів і розширення підстановки на виході, commandперш ніж відображати результат. Тест: що є результатом echo $(echo '*')(і якщо це те, що ви насправді хочете, ви дійсно хочете просто echo *).
трійка

@triplee Я взагалі не використовував echoкоманду. Я просто використав це для демонстрації результату, fooякий з’являється у 3-му рядку як результат 2-го рядка.
Рон

Але просто basename "${file%.*}"зробив би те саме; ви використовуєте підстановку команди для зйомки її виводу, тільки до echoтого самого виводу негайно. (Без цитування, результат номінально відрізняється; але це навряд чи актуально, набагато менша особливість, тут.)
tripleee

Також basename "$file" .txtуникає складності підстановки параметрів.
трійка

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.


Просто хотілося ОСНОВНОГО :) Дякую!
Карлос Рікардо

12

Ви можете змусити вирізати для відображення всіх полів та наступних, додавши -до номера поля.

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

Отже, якщо FILE є eth0.pcap.gz, розширення будеpcap.gz

Використовуючи ту ж логіку, ви також можете отримати ім'я файлу, використовуючи "-" з вирізанням, як описано нижче:

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

Це працює навіть для імен файлів, які не мають жодного розширення.


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. Сподіваюся, це допомагає тим, хто має таку ж проблему =)


Правильний результат (відповідно до того os.path.splitext, чого хоче ОП) ('stuff.tar', '.gz').
Кікер

6

Я використовую наступний сценарій

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

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

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

Вам не знадобиться перша заява awk в останньому прикладі, правда?
BHSPitMonkey

Ви можете уникнути передачі Awk на Awk, зробивши інше split(). awk -F / '{ n=split($2, a, "."); print a[n] }' uses / `як роздільник верхнього рівня, але потім розбиває друге поле .і друкує останній елемент з нового масиву.
трійка

4

Просто використовуйте ${parameter%word}

У вашому випадку:

${FILE%.*}

Якщо ви хочете перевірити це, усі наступні роботи та просто видаліть розширення:

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

2
Чому потік? Це все ще корисно, хоча навколо =знаків не повинно бути пробілів .
SilverWolf - Відновіть Моніку

1
Це чудово працює. Дякую! (тепер у нього немає пробілів навколо рівних знаків, якщо це було причиною того, що це було знято)
Олексій. С.

3

Будуючи з відповіді Петеша , якщо потрібне лише ім'я файлу, і шлях, і розширення можна позбавити в один рядок,

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

Не працювало для мене: "basename: відсутній операнд. Спробуйте" basename --help "для отримання додаткової інформації."
helmy

Дивно, ви впевнені, що використовуєте Bash? У моєму випадку з обома версіями 3.2.25 (стара CentOS) та 4.3.30 (Debian Jessie) вона працює бездоганно.
cvr

Можливо, у назві файлу є пробіл? Спробуйте використовуватиfilename="$(basename "${fullname%.*}")"
Адріан

Другий аргумент до basenameнеобов’язковий, але вказує розширення, щоб зняти його. Заміна все ще може бути корисною, але, можливо, basenameнасправді це не так, оскільки ви можете фактично виконати всі ці заміни оболонками.
трійка

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 .


Чи є спосіб зробити матч нечутливим?
тонікс

2

Якщо ви також хочете дозволити пусті розширення, це найкоротший час, який я міг би придумати:

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

Перший рядок пояснив: він відповідає PATH.EXT або БЕЗЩЕ і замінює його на EXT. Якщо БУЛЬКІСТЬ було узгоджено, група ext не захоплюється.


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=
$ 

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


З усіх рішень це єдиний, який повертає порожній рядок, коли файл не має розширення з: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

З вищенаведених відповідей найкоротший ліній, що імітує Пітона

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

припускаючи, що ваш файл дійсно має розширення, є

EXT="${PATH##*.}"; FILE=$(basename "$PATH" .$EXT)

У мене є суперечки щодо цього. Я роздумую видалити відповідь, люди якось не люблять її.
commonpike

basename не видаляє розширення, а лише шлях.
Девід Каллен

Пройшло так давно, як я переглянув сторінку чоловіка, що забув про варіант SUFFIX.
Девід Каллен

Ви повинні знати, яке розширення ви хочете зняти, перш ніж дізнатися, що вкласти, EXTщоб це черепашки аж донизу. (Також слід уникати всіх великих літер для імен приватних змінних; вони зарезервовані для системних змінних.)
tripleee
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.