Wyodrębnij nazwę pliku i rozszerzenie w Bash

2104

Chcę osobno pobrać nazwę pliku (bez rozszerzenia) i rozszerzenie.

Najlepszym rozwiązaniem, jakie do tej pory znalazłem, jest:

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

Jest to złe, ponieważ nie działa, jeśli nazwa pliku zawiera wiele .znaków. Jeśli, powiedzmy, mam a.b.js, rozważy ai b.jszamiast a.bi js.

Można to łatwo zrobić w Pythonie za pomocą

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

ale wolałbym nie odpalać interpretera Pythona tylko w tym celu, jeśli to możliwe.

Jakieś lepsze pomysły?

ibz
źródło
To pytanie wyjaśnia tę technikę bash i kilka innych pokrewnych.
jjclarkson
28
Stosując poniższe świetne odpowiedzi, nie wklejaj po prostu swojej zmiennej, tak jak tu pokazuję Błędnie: extension="{$filename##*.}" tak jak robiłem przez pewien czas! Przenieś $kręcone poza: w prawo: extension="${filename##*.}"
Chris K
4
Jest to oczywiście nietrywialny problem i dla mnie trudno jest stwierdzić, czy poniższe odpowiedzi są całkowicie poprawne. To niesamowite, że nie jest to wbudowana operacja w (ba) sh (odpowiedzi wydają się implementować funkcję za pomocą dopasowania wzorca). Zdecydowałem się os.path.splitextzamiast tego użyć Pythona jak wyżej ...
Peter Gibson
1
Ponieważ rozszerzenie musi reprezentować naturę pliku, istnieje magiczne polecenie, które sprawdza plik, aby określić jego charakter i zaoferować standardowe rozszerzenie . patrz moja odpowiedź
F. Hauri
2
Pytanie jest przede wszystkim problematyczne, ponieważ ... Z punktu widzenia systemu operacyjnego i ogólnie systemów plików Unix nie ma czegoś takiego jak rozszerzenie pliku. Używać "." rozdzielanie części to ludzka konwencja , która działa tylko tak długo, jak długo ludzie zgadzają się jej przestrzegać. Na przykład w programie „tar” można było nazwać pliki wyjściowe „tar”. prefiks zamiast sufiksu „.tar” - Podaje „tar.somedir” zamiast „somedir.tar”. Z tego powodu nie ma rozwiązania „ogólne, zawsze działa” - musisz napisać kod, który pasuje do twoich konkretnych potrzeb i oczekiwanych nazw plików.
CM

Odpowiedzi:

3498

Najpierw uzyskaj nazwę pliku bez ścieżki:

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

Alternatywnie możesz skupić się na ostatnim „/” ścieżki zamiast „.” który powinien działać, nawet jeśli masz nieprzewidywalne rozszerzenia plików:

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

Możesz sprawdzić dokumentację:

Petesz
źródło
85
Sprawdź gnu.org/software/bash/manual/html_node/ ... pełny zestaw funkcji.
D.Shawley,
24
Dodaj cudzysłowy do „$ fullfile”, inaczej ryzykujesz zerwaniem nazwy pliku.
lhunath
47
Cholera, możesz nawet napisać nazwę pliku = "$ {fullfile ## * /}" i unikać wywoływania dodatkowegobasename
efemerii
45
To „rozwiązanie” nie działa, jeśli plik nie ma rozszerzenia - zamiast tego wyświetlana jest cała nazwa pliku, co jest dość złe, biorąc pod uwagę, że pliki bez rozszerzeń są wszechobecne.
nccc
43
Fix do czynienia z nazwami plików bez rozszerzenia: extension=$([[ "$filename" = *.* ]] && echo ".${filename##*.}" || echo ''). Pamiętaj, że jeśli rozszerzenie jest obecne, zostanie ono zwrócone wraz z początkowym ., np .txt.
mklement0
683
~% FILE="example.tar.gz"

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

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

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

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

Aby uzyskać więcej informacji, zobacz rozszerzenie parametrów powłoki w podręczniku Bash.

Juliano
źródło
22
Ty (być może nieumyślnie) poruszasz doskonałe pytanie, co zrobić, jeśli w części pliku „rozszerzenie” znajdują się 2 kropki, jak w .tar.gz ... Nigdy nie zastanawiałem się nad tym problemem i podejrzewam, że to nie do rozwiązania bez znajomości wszystkich możliwych prawidłowych rozszerzeń plików z góry.
rmeador
8
Dlaczego nie do rozwiązania? W moim przykładzie należy wziąć pod uwagę, że plik zawiera dwa rozszerzenia, a nie rozszerzenie z dwoma kropkami. Oba rozszerzenia są obsługiwane osobno.
Juliano
22
Jest to nierozwiązywalne pod względem leksykalnym, musisz sprawdzić typ pliku. Zastanów się, czy miałeś grę o nazwie dinosaurs.in.tari dinosaurs.in.tar.gz
zgubiłeś
11
To staje się bardziej skomplikowane, jeśli przechodzisz pełnymi ścieżkami. Jeden z moich miał „.” w katalogu pośrodku ścieżki, ale nie w nazwie pliku. Przykład „a / bc / d / e / nazwa pliku” skończyłby się „.c / d / e / nazwa pliku”
Walt Sellers
6
najwyraźniej nie x.tar.gzma rozszerzenia, gza nazwa pliku jest x.tartaka. Nie ma czegoś takiego jak podwójne rozszerzenia. jestem całkiem pewien, że system :: :: system obsługuje to w ten sposób. (split path, change_extension ...) i jego zachowanie jest oparte na pythonie, jeśli się nie mylę.
v.oddou
430

Zwykle znasz już to rozszerzenie, więc możesz chcieć użyć:

basename filename .extension

na przykład:

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

i dostajemy

filename
Tomi Po
źródło
60
Ten drugi argument basenamejest całkiem
atrakcyjny
10
A jak wyodrębnić rozszerzenie, używając tej techniki? ;) Zaczekaj! Tak naprawdę nie wiemy tego z góry.
Tomasz Gandor
3
Załóżmy, że masz spakowany katalog, który kończy się na .ziplub .ZIP. Czy istnieje sposób na zrobienie czegoś takiego basename $file {.zip,.ZIP}?
Dennis
8
Chociaż odpowiada to tylko na część pytania PO, odpowiada na pytanie, które wpisałem w Google. :-) Bardzo zręczny!
sudo make install
1
łatwa i zgodna z POSIX
gpanda
146

Możesz użyć magii rozszerzania parametrów POSIX:

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

Jest zastrzeżenie, że jeśli twoja nazwa pliku ma formę, ./somefile.tar.gzto echo ${FILENAME%%.*}łapczywie usuwa najdłuższe dopasowanie do. i masz pusty ciąg.

(Można to obejść za pomocą zmiennej tymczasowej:

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

)


Ta strona wyjaśnia więcej.

${variable%pattern}
  Trim the shortest match from the end
${variable##pattern}
  Trim the longest match from the beginning
${variable%%pattern}
  Trim the longest match from the end
${variable#pattern}
  Trim the shortest match from the beginning
sotapme
źródło
5
Znacznie prostsze niż odpowiedź Joachima, ale zawsze muszę szukać podstawienia zmiennych POSIX. Działa to również na Max OSX, gdzie cutnie ma --complementi sednie ma -r.
jwadsack
72

To nie wydaje się działać, jeśli plik nie ma rozszerzenia lub nie ma nazwy pliku. Oto czego używam; wykorzystuje tylko wbudowane i obsługuje więcej (ale nie wszystkie) patologiczne nazwy plików.

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

A oto kilka przypadków testowych:

$ 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 / ...
/:
    katalog = "/"
    base = ""
    ext = ""
/ home / ja /:
    katalog = "/ home / me /"
    base = ""
    ext = ""
/ home / me / file:
    katalog = "/ home / me /"
    base = „plik”
    ext = ""
/home/me/file.tar:
    katalog = "/ home / me /"
    base = „plik”
    ext = "tar"
/home/me/file.tar.gz:
    katalog = "/ home / me /"
    base = "plik.tar"
    ext = "gz"
/home/me/.hidden:
    katalog = "/ home / me /"
    base = ".hidden"
    ext = ""
/home/me/.hidden.tar:
    katalog = "/ home / me /"
    base = ".hidden"
    ext = "tar"
/ home / me / ..:
    katalog = "/ home / me /"
    base = „..”
    ext = ""
.:
    katalog = ""
    base = "."
    ext = ""
Doktor J
źródło
2
Zamiast dir="${fullpath:0:${#fullpath} - ${#filename}}"często widziałem dir="${fullpath%$filename}". Łatwiej jest pisać. Nie jestem pewien, czy istnieje jakakolwiek rzeczywista różnica prędkości lub nierówności.
dubiousjim
2
Używa #! / Bin / bash, co prawie zawsze jest złe. Preferuj #! / Bin / sh, jeśli to możliwe lub #! / Usr / bin / env bash, jeśli nie.
Dobra osoba
@Good Person: Nie wiem, jak prawie zawsze jest źle: which bash-> /bin/bash; może to twoja dystrybucja?
vol7ron,
2
@ vol7ron - na wielu dystrybucjach bash znajduje się w / usr / local / bin / bash. W OSX wiele osób instaluje zaktualizowaną wersję bash w / opt / local / bin / bash. Jako taki / bin / bash jest niepoprawny i do znalezienia go należy użyć env. Jeszcze lepsze jest użycie konstrukcji / bin / sh i POSIX. Z wyjątkiem solaris jest to powłoka POSIX.
Dobra osoba,
2
@GoodPerson, ale jeśli czujesz się bardziej komfortowo z bash, po co używać sh? Czy to nie jest powiedzenie, po co używać Perla, skoro możesz używać sh?
vol7ron
46

Możesz użyć basename.

Przykład:

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

Trzeba zapewnić miejscami nazwę z rozszerzeniem, które zostaną usunięte, jednak jeśli są zawsze wykonując tarprzy -zczym wiesz, że rozszerzenie będzie .tar.gz.

To powinno zrobić, co chcesz:

tar -zxvf $1
cd $(basename $1 .tar.gz)
Bjarke Freund-Hansen
źródło
2
Podejrzewam, że cd $(basename $1 .tar.gz)działa na pliki .gz. Ale w pytaniu wspomniałArchive files have several extensions: tar.gz, tat.xz, tar.bz2
SS Hegde
Tomi Po opublikował te same rzeczy 2 lata wcześniej.
phil294
Cześć Blauhirn, łał, to stare pytania. Myślę, że coś się stało z datami. Pamiętam wyraźnie, że odpowiedziałem na pytanie krótko po tym, jak zostało zadane, i tam znalazło się tylko kilka innych odpowiedzi. Czy to możliwe, że pytanie zostało połączone z innym, czy SO to robi?
Bjarke Freund-Hansen
Tak, pamiętam poprawnie. Pierwotnie odpowiadam na to pytanie stackoverflow.com/questions/14703318/... tego samego dnia, w którym zostało zadane, 2 lata później zostało połączone w jedno. Trudno mi winić za duplikat odpowiedzi, kiedy moja odpowiedź została przeniesiona w ten sposób.
Bjarke Freund-Hansen
37
pax> echo a.b.js | sed 's/\.[^.]*$//'
a.b
pax> echo a.b.js | sed 's/^.*\.//'
js

działa dobrze, więc możesz po prostu użyć:

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

Nawiasem mówiąc, polecenia działają w następujący sposób.

Polecenie for NAMEzastępuje "."znak, po którym następuje dowolna liczba "."znaków niebędących znakami aż do końca linii, bez niczego (tzn. Usuwa wszystko od końca "."do końca linii włącznie). Zasadniczo jest to chciwe podstawienie przy użyciu sztuczek wyrażeń regularnych.

Komenda for EXTENSIONzastępuje dowolną liczbę znaków, po której następuje "."znak na początku wiersza, bez niczego (tzn. Usuwa wszystko od początku wiersza do ostatniej kropki włącznie). To jest zachłanna zamiana, która jest domyślną akcją.

paxdiablo
źródło
Ta przerwa dla plików bez rozszerzenia, ponieważ wydrukowałaby to samo dla nazwy i rozszerzenia. Więc używam sed 's,\.[^\.]*$,,'dla nazwy i sed 's,.*\.,., ;t ;g'dla rozszerzenia (używa poleceń nietypowych testi getpoleceń, wraz z typowym substitutepoleceniem).
hIpPy
32

Mellen pisze w komentarzu do posta na blogu:

Korzystając z Bash, istnieje również ${file%.*}możliwość pobrania nazwy pliku bez rozszerzenia i ${file##*.}uzyskania samego rozszerzenia. To jest,

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

Wyjścia:

filename: thisfile
extension: txt
Kebabbert
źródło
2
@REACHUS: Zobacz gnu.org/software/bash/manual/html_node/…
mklement0
29

Nie trzeba przejmować się awkani sednawet perldla tego prostego zadania. Istnieje os.path.splitext()rozwiązanie zgodne z czystym Bash, które wykorzystuje tylko rozszerzenia parametrów.

Wdrożenie referencyjne

Dokumentacja os.path.splitext(path):

Podziel ścieżkę nazwy ścieżki na parę, (root, ext)tak aby root + ext == pathi ext był pusty lub zaczyna się kropką i zawiera co najwyżej jeden kropkę. Okresy wiodące w basenieame są ignorowane; splitext('.cshrc')zwraca ('.cshrc', '').

Kod Python:

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

Implementacja Bash

Uhonorowanie wiodących okresów

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

Ignorowanie okresów wiodących

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

Testy

Oto przypadki testowe dla implementacji Ignorowanie okresów wiodących , które powinny pasować do implementacji referencyjnej Python na każdym wejściu.

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

Wyniki testów

Wszystkie testy przeszły pomyślnie.

Cyker
źródło
2
nie, podstawową nazwą pliku text.tar.gzpowinna być, texta rozszerzenie be.tar.gz
frederick99
2
@ frederick99 Jak powiedziałem, rozwiązanie tutaj odpowiada implementacji os.path.splitextw Pythonie. To, czy wdrożenie jest rozsądne w przypadku potencjalnie kontrowersyjnych danych wejściowych, to kolejny temat.
Cyker,
Jak działają cytaty wewnątrz wzorca ( "$root")? Co by się stało, gdyby zostały pominięte? (Nie mogłem znaleźć żadnej dokumentacji w tej sprawie.) Również w jaki sposób obsługuje te nazwy plików z nimi *lub ?w nich?
ymett
Ok, testowanie pokazuje mi, że cytaty sprawiają, że wzór jest dosłowny, tj. *I ?nie są wyjątkowe. Tak więc dwie części mojego pytania odpowiadają sobie nawzajem. Czy mam rację, że nie jest to udokumentowane? A może należy to rozumieć z faktu, że cytaty ogólnie wyłączają ekspansję globalną?
ymett
Genialna odpowiedź! Po prostu zasugeruję nieco prostszy wariant obliczania katalogu głównego: root="${path#?}";root="${path::1}${root%.*}"- następnie postępuj tak samo, aby wyodrębnić rozszerzenie.
Maëlan
26

Możesz użyć cutpolecenia, aby usunąć dwa ostatnie rozszerzenia ( ".tar.gz"część):

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

Jak zauważył Clayton Hughes w komentarzu, nie zadziała to w przypadku rzeczywistego przykładu w pytaniu. Jako alternatywę proponuję używać sedrozszerzonych wyrażeń regularnych, takich jak:

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

Działa poprzez bezwarunkowe usunięcie dwóch ostatnich (alfanumerycznych) rozszerzeń.

[Zaktualizowany ponownie po komentarzu Andersa Lindahla]

Jakiś koleś programisty
źródło
4
Działa to tylko w przypadku, gdy nazwa pliku / ścieżka nie zawiera żadnych innych kropek: echo "mpc-1.0.1.tar.gz" | cut -d '.' --complement -f2- tworzy „mpc-1” (tylko pierwsze 2 pola po ograniczeniu przez.)
Clayton Hughes
@ClaytonHughes Masz rację i powinienem był to sprawdzić lepiej. Dodano inne rozwiązanie.
Jakiś programista koleś
Wyrażenia sed powinny używać $do sprawdzania, czy dopasowane rozszerzenie znajduje się na końcu nazwy pliku. W przeciwnym razie nazwa pliku i.like.tar.gz.files.tar.bz2może spowodować nieoczekiwany wynik.
Anders Lindahl,
@AndersLindahl Tak będzie, jeśli kolejność rozszerzeń jest odwrotna do sedkolejności łańcucha. Nawet z $na końcu nazwą pliku, mpc-1.0.1.tar.bz2.tar.gzktóra usunie oba, .tar.gza następnie .tar.bz2.
Jakiś programista koleś
$ echo "foo.tar.gz" | cut -d '.' -f2- BEZ - dopełnienie spowoduje 2. podzielony element na końcu ciągu $ echo "foo.tar.gz" | cut -d '.' -f2- tar.gz
Gene Black
23

Oto kilka alternatywnych sugestii (głównie w awk), w tym niektóre zaawansowane przypadki użycia, takie jak wyodrębnianie numerów wersji pakietów oprogramowania.

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 '^[/]\|^~/'

Wszystkie przypadki użycia wykorzystują oryginalną pełną ścieżkę jako dane wejściowe, bez zależności od wyników pośrednich.

henfiber
źródło
20

Odpowiedź Akceptowane działa dobrze w typowych przypadkach , ale nie w krawędziowych przypadkach , a mianowicie:

  • W przypadku nazw plików bez rozszerzenia (zwanych sufiksem w pozostałej części tej odpowiedzi) extension=${filename##*.}zwraca nazwę pliku wejściowego zamiast pustego ciągu.
  • extension=${filename##*.}nie obejmuje początkowej ., sprzecznej z konwencją.
    • Ślepe dodawanie .nie działałoby w przypadku nazw plików bez sufiksu.
  • filename="${filename%.*}"będzie pustym ciągiem, jeśli nazwa pliku wejściowego zaczyna się od .i nie zawiera dalszych .znaków (np. .bash_profile) - w przeciwieństwie do konwencji.

---------

Zatem złożoność solidnego rozwiązania, które obejmuje wszystkie przypadki brzegowe, wymaga funkcji - patrz jej definicja poniżej; to może wrócić wszystkie składniki ścieżki .

Przykładowe wywołanie:

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

Zauważ, że argumenty za ścieżką wejściową są dowolnie wybieranymi nazwami zmiennych pozycyjnych .
Aby pominąć zmienne niebędące przedmiotem zainteresowania, które występują przed tymi, które są, określ _(aby użyć zmiennej wyrzucania $_) lub ''; np. aby wyodrębnić katalog główny i rozszerzenie tylko, użyj 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

Kod testowy, który wykonuje funkcję:

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

Oczekiwany wynik - zwróć uwagę na przypadki na krawędziach:

  • nazwa pliku bez przyrostka
  • nazwa pliku rozpoczynająca się od .( nie uważana za początek sufiksu)
  • ścieżka wejściowa kończąca się na /(końcowe/ jest ignorowany)
  • ścieżka wejściowa, która jest tylko nazwą pliku ( .jest zwracana jako ścieżka nadrzędna)
  • nazwa pliku, która ma więcej niż .prefiks token (tylko ostatni jest uważany za przyrostek):
----- /etc/bash.bashrc
parentpath=/etc
fname=bash.bashrc
fnameroot=bash
suffix=.bashrc
----- /usr/bin/grep
parentpath=/usr/bin
fname=grep
fnameroot=grep
suffix=
----- /Users/jdoe/.bash_profile
parentpath=/Users/jdoe
fname=.bash_profile
fnameroot=.bash_profile
suffix=
----- /Library/Application Support/
parentpath=/Library
fname=Application Support
fnameroot=Application Support
suffix=
----- readme.new.txt
parentpath=.
fname=readme.new.txt
fnameroot=readme.new
suffix=.txt
mklement0
źródło
19

Najmniejsze i najprostsze rozwiązanie (w jednej linii) to:

$ file=/blaabla/bla/blah/foo.txt
echo $(basename ${file%.*}) # foo
Ron
źródło
To bezużyteczne użycieecho . Ogólnie rzecz biorąc, echo $(command)jest lepiej napisany po prostu, commandchyba że specjalnie potrzebujesz, aby powłoka wykonała tokenizację białych znaków i interpretację symboli wieloznacznych na wyjściu commandprzed wyświetleniem wyniku. Quiz: jaki jest wynik echo $(echo '*')(a jeśli tego naprawdę chcesz, naprawdę chcesz echo *.)
tripleee
@triplee W ogóle nie użyłem echopolecenia. Właśnie użyłem go do zademonstrowania wyniku, fooktóry pojawia się w 3. linii jako wynik 2. linii.
Ron
Ale po prostu basename "${file%.*}"zrobiłbym to samo; używasz podstawienia polecenia, aby przechwycić jego wynik, tylko do echotego samego wyniku natychmiast. (Bez cytowania wynik jest nominalnie inny; ale to nie jest istotne, a tym bardziej cecha tutaj.)
tripleee
basename "$file" .txtPozwala także uniknąć złożoności podstawiania parametrów.
tripleee
1
@ Ron Przeczytaj swój pierwszy komentarz, zanim oskarży go o marnowanie naszego czasu.
frederick99
14

Myślę, że jeśli potrzebujesz tylko nazwy pliku, możesz spróbować:

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"

I to wszystko = D.

Andrew Woolfgang
źródło
Właśnie chciałem BASEDIRECTORY :) Dzięki!
Carlos Ricardo,
12

Możesz wymusić wycięcie, aby wyświetlić wszystkie pola i kolejne dodające -do numeru pola.

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

Więc jeśli PLIK jest eth0.pcap.gz, ROZSZERZENIE będziepcap.gz

Korzystając z tej samej logiki, możesz również pobrać nazwę pliku za pomocą „-” z cięciem w następujący sposób:

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

Działa to nawet w przypadku nazw plików, które nie mają żadnego rozszerzenia.

maciek gajewski
źródło
8

Rozpoznawanie plików magicznych

Oprócz wielu dobrych odpowiedzi na to pytanie dotyczące przepełnienia stosu chciałbym dodać:

W Linuksie i innych systemach uniksowych istnieje magiczne polecenie o nazwie file, które wykrywa typ pliku, analizując kilka pierwszych bajtów pliku. To bardzo stare narzędzie, początkowo używane do serwerów wydruku (jeśli nie zostało stworzone dla ... Nie jestem tego pewien).

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

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

Rozszerzenia standardów można znaleźć w /etc/mime.types(na moim pulpicie Debian GNU / Linux. Zobacz man filei man mime.types. Być może musisz zainstalować filenarzędzie i mime-supportpakiety):

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

Możesz utworzyć funkcja określania właściwego rozszerzenia. Jest mała (nie idealna) próbka:

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##*[/.-]}
}

Ta funkcja może ustawić zmienną Bash, której można później użyć:

(Jest to inspirowane prawidłową odpowiedzią @Petesh):

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

echo "$fullfile -> $filename . $extension"
F. Hauri
źródło
8

Ok, więc jeśli dobrze rozumiem, problem polega na tym, jak uzyskać nazwę i pełne rozszerzenie pliku, który ma wiele rozszerzeń, np. stuff.tar.gz .

To działa dla mnie:

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

To da ci stuffnazwę pliku i .tar.gzrozszerzenie. Działa dla dowolnej liczby rozszerzeń, w tym 0. Mam nadzieję, że pomoże to każdemu, kto ma ten sam problem =)

Al3xXx
źródło
Prawidłowy wynik (zgodnie z tym os.path.splitext, czego chce PO) jest ('stuff.tar', '.gz').
Cyker,
6

Używam następującego skryptu

$ echo "foo.tar.gz"|rev|cut -d"." -f3-|rev
foo
Joydip Datta
źródło
To wcale nie jest wydajne. Przesadzanie za dużo razy, co jest dość niepotrzebne, ponieważ operację tę można wykonać w czystym Bashu bez potrzeby wykonywania zewnętrznych poleceń i rozwidlania.
codeforester
5
$ F = "text file.test.txt"  
$ echo ${F/*./}  
txt  

Obsługuje wiele kropek i spacji w nazwie pliku, jednak jeśli nie ma rozszerzenia, zwraca nazwę pliku. Łatwo to jednak sprawdzić; po prostu sprawdź, czy nazwa pliku i rozszerzenie są takie same.

Oczywiście ta metoda nie działa w przypadku plików .tar.gz. Można to jednak rozwiązać w dwuetapowym procesie. Jeśli rozszerzenie to gz, sprawdź ponownie, czy jest również rozszerzenie tar.

Miriam English
źródło
5

Jak wyodrębnić nazwę pliku i rozszerzenie w rybach :

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

Ostrzeżenia: Dzieli na ostatnią kropkę, co działa dobrze w przypadku nazw plików z kropkami, ale nie jest dobre w przypadku rozszerzeń z kropkami. Zobacz przykład poniżej.

Stosowanie:

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

Prawdopodobnie są na to lepsze sposoby. Edytuj swoją odpowiedź, aby ją poprawić.


Jeśli istnieje ograniczony zestaw rozszerzeń, z którymi będziesz mieć do czynienia i znasz je wszystkie, spróbuj tego:

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

To nie ma zastrzeżenia jako pierwszego przykładu, ale musisz poradzić sobie z każdą sprawą, więc może być bardziej nużąca w zależności od tego, ile rozszerzeń możesz się spodziewać.

Dennis
źródło
4

Oto kod z AWK . Można to zrobić prościej. Ale nie jestem dobry w AWK.

filename$ ls
abc.a.txt  a.b.c.txt  pp-kk.txt
filename$ find . -type f | awk -F/ '{print $2}' | rev | awk -F"." '{$1="";print}' | rev | awk 'gsub(" ",".") ,sub(".$", "")'
abc.a
a.b.c
pp-kk
filename$ find . -type f | awk -F/ '{print $2}' | awk -F"." '{print $NF}'
txt
txt
txt
smilyface
źródło
Nie powinieneś potrzebować pierwszej instrukcji awk w ostatnim przykładzie, prawda?
BHSPitMonkey
Możesz uniknąć przesyłania Awk do Awk, wykonując inną split(). awk -F / '{ n=split($2, a, "."); print a[n] }' uses / `jako ogranicznik najwyższego poziomu, ale następnie dzieli drugie pola .i drukuje ostatni element z nowej tablicy.
tripleee
4

Po prostu użyj ${parameter%word}

W Twoim przypadku:

${FILE%.*}

Jeśli chcesz to przetestować, wykonaj następujące czynności i po prostu usuń rozszerzenie:

FILE=abc.xyz; echo ${FILE%.*};
FILE=123.abc.xyz; echo ${FILE%.*};
FILE=abc; echo ${FILE%.*};
enyo
źródło
2
Dlaczego głosowanie negatywne? Nadal jest przydatny, chociaż wokół =znaków nie powinno być odstępów .
SilverWolf - Przywróć Monikę
1
To działa dobrze. Dziękuję Ci! (teraz nie ma spacji wokół znaków równości, jeśli właśnie dlatego został odrzucony)
Alex. S.
3

Opierając się na odpowiedzi z Petesh , jeśli potrzebna jest tylko nazwa pliku, zarówno ścieżkę, jak i rozszerzenie można usunąć w jednym wierszu,

filename=$(basename ${fullname%.*})
CVR
źródło
Nie działało dla mnie: „basename: missing operand Spróbuj„ basename --help ”, aby uzyskać więcej informacji.”
helmy
Dziwne, czy na pewno używasz Bash? W moim przypadku, w obu wersjach 3.2.25 (stary CentOS) i 4.3.30 (Debian Jessie) działa bezbłędnie.
cvr
Może w nazwie pliku jest spacja? Spróbuj użyćfilename="$(basename "${fullname%.*}")"
Adrian
Drugi argument basenamejest opcjonalny, ale określa rozszerzenie do usunięcia. Podstawienie może być nadal przydatne, ale być może basenamenie jest, ponieważ wszystkie te podstawienia można wykonać za pomocą wbudowanych powłok.
tripleee
3

Oparte w dużej mierze na doskonałej @ mklement0 i pełne losowych, przydatnych bashism - a także innych odpowiedzi na to / inne pytania / „ten cholerny internet” ... podsumowałem to wszystko trochę, nieco bardziej zrozumiale, funkcja wielokrotnego użytku dla mojego (lub twojego), .bash_profilektóry dba o to, co (uważam) powinno być bardziej niezawodną wersją dirname/ basename/ co masz ...

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
}     

Przykłady użycia ...

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>
Alex Gray
źródło
1
Ładnie wykonane; kilka sugestii: - Wygląda na to, że $IFSw ogóle nie polegasz (a jeśli tak, możesz użyć go localdo zlokalizowania efektu ustawienia). - Lepiej używać localzmiennych. - Twój komunikat o błędzie powinien zostać wyświetlony stderr, a nie stdout(użyj 1>&2), i powinieneś zwrócić niezerowy kod wyjścia. - Lepiej zmienić nazwę fullnamena basename(pierwsza sugeruje ścieżkę z komponentami katalogu). - namebezwarunkowo dołącza .(kropkę), nawet jeśli oryginał nie miał. Możesz po prostu użyć basenamenarzędzia, ale pamiętaj, że ignoruje ono zakończenie /.
mklement0
2

Prosta odpowiedź:

Aby rozwinąć na POSIX zmiennych odpowiedzi należy pamiętać, że można zrobić więcej ciekawych wzorów. Dlatego w przypadku opisanym tutaj możesz po prostu to zrobić:

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

To odetnie ostatnie wystąpienie .tar. <coś> .

Mówiąc bardziej ogólnie, jeśli chcesz usunąć ostatnie wystąpienie. <coś> . <something-else> następnie

${1.*.*}

powinien działać dobrze.

Link do powyższej odpowiedzi wydaje się martwy. Oto świetne wyjaśnienie wielu manipulacji ciągami, które możesz wykonać bezpośrednio w Bash, z TLDP .

RandyP
źródło
Czy istnieje sposób, aby dopasowanie nie uwzględniało wielkości liter?
tonix
2

Jeśli chcesz także zezwolić na puste rozszerzenia, jest to najkrótsza z możliwych:

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

Wyjaśnienie pierwszej linii: Pasuje do PATH.EXT lub INNE i zastępuje ją EXT. Jeśli dopasowano WSZYSTKIE, grupa ext nie jest przechwytywana.

phil294
źródło
2

To jedyny, który działał dla mnie:

path='folder/other_folder/file.js'

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

>> file

Można tego również użyć w interpolacji ciągów, ale niestety trzeba to basewcześniej ustawić .

Ken Mueller
źródło
1

Oto algorytm, którego użyłem do znalezienia nazwy i rozszerzenia pliku, kiedy napisałem skrypt Bash, aby uczynić nazwy unikalnymi, gdy nazwy są w konflikcie ze względu na wielkość liter.

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

Uruchomienie testowe

$ 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: Kompletny program transliteracji i więcej przypadków testowych można znaleźć tutaj: https://www.dropbox.com/s/4c6m0f2e28a1vxf/avoid-clashes-code.zip?dl=0

historystamp
źródło
Ze wszystkich rozwiązań jest to jedyne, które zwraca pusty ciąg, gdy plik nie ma rozszerzenia o:extension=$([[ "$theFileName" == *.* ]] && echo ".${theFileName##*.}" || echo '')
f0nzie
1

Korzystając z przykładowego pliku /Users/Jonathan/Scripts/bash/MyScript.sh, ten kod:

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

spowoduje ${ME}bycie MyScripti ${MY_EXT}bycie .sh:


Scenariusz:

#!/bin/bash
set -e

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

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

Niektóre testy:

$ ./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
chown
źródło
2
Nie jestem pewien, dlaczego ma tak wiele głosów negatywnych - jest to faktycznie bardziej wydajne niż zaakceptowana odpowiedź. (W tym drugim przypadku łamie także wejściowe nazwy plików bez rozszerzenia). Korzystanie z wyraźnej ścieżki do basenamemoże być przesadą.
mklement0
1

Z powyższych odpowiedzi, najkrótszy oneliner naśladuje Pythona

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

zakładając, że plik naprawdę ma rozszerzenie, jest

EXT="${PATH##*.}"; FILE=$(basename "$PATH" .$EXT)
pospolity
źródło
Mam na ten temat negatywne opinie. Zastanawiam się nad usunięciem odpowiedzi, ludzie jakoś jej nie lubią.
commonpike,
basename nie usuwa rozszerzenia, tylko ścieżkę.
David Cullen
Minęło tyle czasu, odkąd spojrzałem na stronę podręcznika, że ​​zapomniałem o opcji SUFFIX.
David Cullen,
Musisz wiedzieć, które rozszerzenie chcesz zdjąć, zanim będziesz wiedział, w co je włożyć, EXTwięc są to żółwie na całej długości. (Ponadto należy unikać
używania