Ekstrak nama file dan ekstensi di Bash


2110

Saya ingin mendapatkan nama file (tanpa ekstensi) dan ekstensi secara terpisah.

Solusi terbaik yang saya temukan sejauh ini adalah:

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

Ini salah karena tidak berfungsi jika nama file berisi banyak .karakter. Jika, katakanlah, saya punya a.b.js, itu akan mempertimbangkan adan b.js, bukannya a.bdan js.

Ini dapat dengan mudah dilakukan dengan Python

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

tapi saya lebih suka untuk tidak menyalakan interpreter Python hanya untuk ini, jika memungkinkan.

Ada ide yang lebih baik?


Pertanyaan ini menjelaskan teknik bash ini dan beberapa yang terkait lainnya.
jjclarkson

28
Ketika menerapkan jawaban yang bagus di bawah, jangan hanya menempelkan variabel Anda seperti yang saya tunjukkan di sini salah: extension="{$filename##*.}" seperti yang saya lakukan untuk sementara waktu! Pindahkan bagian $luar keriting: Kanan: extension="${filename##*.}"
Chris K

4
Ini jelas merupakan masalah yang tidak sepele dan bagi saya sulit untuk mengatakan apakah jawaban di bawah ini sepenuhnya benar. Sungguh menakjubkan ini bukan operasi built in di (ba) sh (jawaban tampaknya menerapkan fungsi menggunakan pencocokan pola). Saya memutuskan untuk menggunakan Python os.path.splitextseperti di atas sebagai gantinya ...
Peter Gibson

1
Sebagai ekstensi harus mewakili sifat file, ada perintah ajaib yang memeriksa file untuk mengetahui sifatnya dan memberikan ekstensi standar . lihat jawaban saya
F. Hauri

2
Pertanyaannya bermasalah di tempat pertama karena .. Dari perspektif OS dan sistem file unix secara umum, tidak ada yang namanya ekstensi file. Menggunakan sebuah "." untuk memisahkan bagian adalah konvensi manusia , yang hanya berfungsi selama manusia setuju untuk mengikutinya. Misalnya, dengan program 'tar', bisa diputuskan untuk memberi nama file output dengan "tar." awalan alih-alih sufiks ".tar" - Memberikan "tar.somedir" alih-alih "somedir.tar". Tidak ada solusi "umum, selalu berfungsi" karena ini - Anda harus menulis kode yang sesuai dengan kebutuhan spesifik Anda dan nama file yang diharapkan.
CM

Jawaban:


3504

Pertama, dapatkan nama file tanpa path:

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

Sebagai alternatif, Anda dapat fokus pada '/' terakhir dari jalur alih-alih '.' yang seharusnya berfungsi meskipun Anda memiliki ekstensi file yang tidak dapat diprediksi:

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

Anda mungkin ingin memeriksa dokumentasinya:


85
Lihat gnu.org/software/bash/manual/html_node/… untuk set fitur lengkap.
D.Shawley

24
Tambahkan beberapa kutipan ke "$ fullfile", atau Anda akan berisiko melanggar nama file.
lhunath

47
Heck, Anda bahkan dapat menulis nama file = "$ {fullfile ## * /}" dan menghindari memanggil ekstrabasename
ephemient

45
"Solusi" ini tidak berfungsi jika file tidak memiliki ekstensi - sebagai gantinya, seluruh nama file adalah output, yang cukup buruk mengingat bahwa file tanpa ekstensi ada di mana-mana.
nccc

43
Perbaiki untuk menangani nama file tanpa ekstensi: extension=$([[ "$filename" = *.* ]] && echo ".${filename##*.}" || echo ''). Catatan bahwa jika perpanjangan adalah hadir, itu akan dikembalikan termasuk awal ., misalnya, .txt.
mklement0

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

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

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

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

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

Untuk detail lebih lanjut, lihat ekspansi parameter shell di manual Bash.


22
Anda (mungkin tidak sengaja) memunculkan pertanyaan bagus tentang apa yang harus dilakukan jika bagian "ekstensi" dari nama file memiliki 2 titik di dalamnya, seperti di .tar.gz ... Saya tidak pernah mempertimbangkan masalah itu, dan saya menduga itu tidak dapat dipecahkan tanpa mengetahui semua kemungkinan ekstensi file yang valid di depan.
rmeador

8
Kenapa tidak dipecahkan? Dalam contoh saya, harus dipertimbangkan bahwa file berisi dua ekstensi, bukan ekstensi dengan dua titik. Anda menangani kedua ekstensi secara terpisah.
Juliano

22
Tidak dapat dipecahkan berdasarkan leksikal, Anda harus memeriksa jenis file. Pertimbangkan jika Anda memiliki permainan yang dipanggil dinosaurs.in.tardan Anda gzip ke dinosaurs.in.tar.gz:)
porges

11
Ini menjadi lebih rumit jika Anda melewati jalur penuh. Salah satu milikku punya '.' dalam direktori di tengah jalan, tetapi tidak ada dalam nama file. Contoh "a / bc / d / e / filename" akan berakhir ".c / d / e / filename"
Walt Sellers

6
jelas tidak ada x.tar.gzekstensi gzdan nama file x.taritu dia. Tidak ada yang namanya ekstensi ganda. Saya cukup yakin boost :: filesystem menanganinya seperti itu. (path split, change_extension ...) dan perilakunya didasarkan pada python jika saya tidak salah.
v.oddou

431

Biasanya Anda sudah tahu ekstensi, jadi Anda mungkin ingin menggunakan:

basename filename .extension

sebagai contoh:

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

dan kita dapatkan

filename

61
Argumen kedua basenameyang cukup membuka mata, baik jenis sir / madam :)
akaIDIOT

10
Dan bagaimana cara mengekstrak ekstensi, menggunakan teknik ini? ;) Oh tunggu! Kami sebenarnya tidak tahu itu di muka.
Tomasz Gandor

3
Katakanlah Anda memiliki direktori zip yang diakhiri dengan .zipatau .ZIP. Apakah ada cara Anda bisa melakukan sesuatu seperti basename $file {.zip,.ZIP}?
Dennis

8
Meskipun ini hanya menjawab sebagian dari pertanyaan OP, itu menjawab pertanyaan yang saya ketikkan di google. :-) Sangat apik!
sudo make install

1
mudah dan sesuai POSIX
gpanda

147

Anda dapat menggunakan keajaiban ekspansi parameter POSIX:

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

Ada peringatan bahwa jika nama file Anda berbentuk ./somefile.tar.gzmaka echo ${FILENAME%%.*}rakus akan menghapus pertandingan terpanjang ke .dan Anda akan memiliki string kosong.

(Anda bisa mengatasinya dengan variabel sementara:

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

)


Situs ini menjelaskan lebih lanjut.

${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
Jauh lebih sederhana daripada jawaban Joachim tetapi saya selalu harus mencari subtitusi variabel POSIX. Juga, ini berjalan pada Max OSX di mana cuttidak memiliki --complementdan sedtidak memiliki -r.
jwadsack

72

Tampaknya tidak berfungsi jika file tidak memiliki ekstensi, atau tanpa nama file. Inilah yang saya gunakan; hanya menggunakan builtin dan menangani lebih banyak (tetapi tidak semua) nama file patologis.

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

Dan inilah beberapa testcases:

$ basename-and-extension.sh / / home / me / / home / me / file / home / me / file.tar / home / me / file.tar.gz / rumah / me / .hidden / home / me / .hidden.tar / home / me / ...
/:
    dir = "/"
    base = ""
    ext = ""
/ rumah / saya /:
    dir = "/ home / me /"
    base = ""
    ext = ""
/ home / me / file:
    dir = "/ home / me /"
    base = "file"
    ext = ""
/home/me/file.tar:
    dir = "/ home / me /"
    base = "file"
    ext = "tar"
/home/me/file.tar.gz:
    dir = "/ home / me /"
    base = "file.tar"
    ext = "gz"
/home/me/.hidden:
    dir = "/ home / me /"
    base = ".hidden"
    ext = ""
/home/me/.hidden.tar:
    dir = "/ home / me /"
    base = ".hidden"
    ext = "tar"
/ home / me / ..:
    dir = "/ home / me /"
    base = ".."
    ext = ""
.:
    dir = ""
    base = "."
    ext = ""

2
Bukannya dir="${fullpath:0:${#fullpath} - ${#filename}}"saya sudah sering melihat dir="${fullpath%$filename}". Lebih mudah untuk menulis. Tidak yakin apakah ada perbedaan kecepatan nyata atau gotcha.
dubiousjim

2
Ini menggunakan #! / Bin / bash yang hampir selalu salah. Lebih suka #! / Bin / sh jika mungkin atau #! / Usr / bin / env bash jika tidak.
Orang Baik

@Good Person: Saya tidak tahu bagaimana ini hampir selalu salah: which bash-> /bin/bash; mungkin itu distro kamu?
vol7ron

2
@ vol7ron - pada banyak distro, bash ada di / usr / local / bin / bash. Di OSX banyak orang memasang bash yang diperbarui di / opt / local / bin / bash. Karena itu / bin / bash salah dan kita harus menggunakan env untuk menemukannya. Yang lebih baik lagi adalah menggunakan konstruksi / bin / sh dan POSIX. Kecuali pada solaris, ini adalah shell POSIX.
Orang Baik

2
@ GoodPerson tetapi jika Anda lebih nyaman dengan bash, mengapa menggunakan sh? Bukankah itu seperti mengatakan, mengapa menggunakan Perl saat Anda dapat menggunakan sh?
vol7ron

46

Anda bisa menggunakannya basename.

Contoh:

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

Anda perlu menyediakan basename dengan ekstensi yang akan dihapus, namun jika Anda selalu mengeksekusi tardengan -zmaka Anda tahu ekstensi akan .tar.gz.

Ini harus melakukan apa yang Anda inginkan:

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

2
Saya kira cd $(basename $1 .tar.gz)berfungsi untuk file .gz. Tetapi dalam pertanyaan yang dia sebutkanArchive files have several extensions: tar.gz, tat.xz, tar.bz2
SS Hegde

Tomi Po memposting hal yang sama 2 tahun sebelumnya.
phil294

Hai Blauhirn, ini pertanyaan lama. Saya pikir sesuatu telah terjadi pada tanggal. Saya ingat secara khusus menjawab pertanyaan tidak lama setelah itu diajukan, dan di sana hanya ada beberapa jawaban lain. Mungkinkah pertanyaannya digabung dengan yang lain, apakah SO melakukan itu?
Bjarke Freund-Hansen

Yap saya ingat dengan benar. Saya awalnya menjawab pertanyaan ini stackoverflow.com/questions/14703318/… pada hari yang sama ketika ditanya, 2 tahun kemudian digabung menjadi yang ini. Saya hampir tidak dapat disalahkan atas jawaban rangkap ketika jawaban saya dipindahkan dengan cara ini.
Bjarke Freund-Hansen

37
pax> echo a.b.js | sed 's/\.[^.]*$//'
a.b
pax> echo a.b.js | sed 's/^.*\.//'
js

berfungsi dengan baik, jadi Anda bisa menggunakan:

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

Omong-omong, perintahnya bekerja sebagai berikut.

Perintah untuk NAMEmengganti "."karakter yang diikuti oleh sejumlah non- "."karakter hingga akhir baris, tanpa apa-apa (yaitu, menghapus semua dari akhir "."hingga akhir baris, termasuk). Ini pada dasarnya adalah substitusi non-serakah menggunakan tipu daya regex.

Perintah untuk EXTENSIONmengganti sejumlah karakter diikuti oleh "."karakter di awal baris, tanpa apa-apa (yaitu, menghapus semua dari awal baris ke titik akhir, termasuk). Ini adalah pengganti serakah yang merupakan tindakan default.


Ini istirahat untuk file tanpa ekstensi karena akan mencetak sama untuk nama dan ekstensi. Jadi saya gunakan sed 's,\.[^\.]*$,,'untuk nama, dan sed 's,.*\.,., ;t ;g'untuk ekstensi (menggunakan atipikal testdan getperintah, bersama dengan substituteperintah khas ).
hIpPy

32

Mellen menulis dalam komentar di posting blog:

Menggunakan Bash, ada juga ${file%.*}untuk mendapatkan nama file tanpa ekstensi dan ${file##*.}untuk mendapatkan ekstensi itu sendiri. Itu adalah,

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

Output:

filename: thisfile
extension: txt


29

Tidak perlu repot dengan awkatau sedatau bahkan perluntuk tugas sederhana ini. Ada os.path.splitext()solusi Bash murni, -compatible yang hanya menggunakan ekspansi parameter.

Implementasi Referensi

Dokumentasi os.path.splitext(path):

Membagi jalur pathname menjadi pasangan (root, ext)sedemikian rupa root + ext == path, dan ext kosong atau dimulai dengan tanda titik dan berisi paling banyak satu periode. Periode terkemuka pada nama dasar diabaikan; splitext('.cshrc')kembali ('.cshrc', '').

Kode python:

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

Implementasi Bash

Menghormati periode terkemuka

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

Mengabaikan periode memimpin

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

Tes

Berikut ini adalah kasus uji untuk penerapan Periode mengabaikan terkemuka , yang harus cocok dengan implementasi referensi Python pada setiap input.

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

Hasil tes

Semua tes lulus.


2
tidak, nama file dasar text.tar.gzseharusnya textdan ekstensi menjadi.tar.gz
frederick99

2
@ frederick99 Seperti yang saya katakan solusi di sini cocok dengan implementasi os.path.splitextdi Python. Apakah implementasi itu masuk akal untuk input yang mungkin kontroversial adalah topik lain.
Cyker

Bagaimana cara kerja kutipan dalam pola ( "$root")? Apa yang bisa terjadi jika mereka dihilangkan? (Saya tidak dapat menemukan dokumentasi tentang masalah ini.) Juga bagaimana ini menangani nama file dengan *atau ?di dalamnya?
ymett

Ok, pengujian menunjukkan kepada saya bahwa tanda kutip membuat pola menjadi literal, yaitu *dan ?tidak istimewa. Jadi dua bagian dari pertanyaan saya saling menjawab. Apakah saya benar bahwa ini tidak didokumentasikan? Atau apakah ini seharusnya dipahami dari fakta bahwa kutipan menonaktifkan ekspansi glob secara umum?
ymett

Jawaban yang brilian! Saya hanya akan menyarankan varian yang sedikit lebih sederhana untuk menghitung root: root="${path#?}";root="${path::1}${root%.*}"- lalu lanjutkan sama untuk mengekstrak ekstensi.
Maëlan

26

Anda bisa menggunakan cutperintah untuk menghapus dua ekstensi terakhir ( ".tar.gz"bagian):

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

Seperti dicatat oleh Clayton Hughes dalam komentar, ini tidak akan berfungsi untuk contoh aktual dalam pertanyaan. Jadi sebagai alternatif saya usulkan menggunakan seddengan ekspresi reguler yang diperluas, seperti ini:

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

Ia bekerja dengan menghapus dua ekstensi terakhir (alfanumerik) tanpa syarat.

[Diperbarui lagi setelah komentar dari Anders Lindahl]


4
Ini hanya berfungsi dalam kasus di mana nama file / path tidak mengandung titik-titik lain: echo "mpc-1.0.1.tar.gz" | cut -d '.' --complement -f2- menghasilkan "mpc-1" (hanya 2 bidang pertama setelah pembatas oleh.)
Clayton Hughes

@ClaytonHughes Anda benar, dan saya harus mengujinya lebih baik. Menambahkan solusi lain.
Beberapa programmer dude

Ekspresi sed harus digunakan $untuk memeriksa bahwa ekstensi yang cocok ada di akhir nama file. Kalau tidak, nama file seperti i.like.tar.gz.files.tar.bz2mungkin menghasilkan hasil yang tidak terduga.
Anders Lindahl

@AndersLindahl Masih akan, jika urutan ekstensi adalah kebalikan dari sedurutan rantai. Bahkan dengan $di akhir nama file seperti mpc-1.0.1.tar.bz2.tar.gzakan menghapus keduanya .tar.gzdan kemudian .tar.bz2.
Beberapa programmer Bung

$ echo "foo.tar.gz" | cut -d '.' -f2- TANPA --complement akan mendapatkan item split ke-2 di akhir string $ echo "foo.tar.gz" | cut -d '.' -f2- tar.gz
Gene Black

23

Berikut adalah beberapa saran alternatif (kebanyakan dalam awk), termasuk beberapa kasus penggunaan lanjutan, seperti mengekstraksi nomor versi untuk paket perangkat lunak.

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

Semua kasing menggunakan jalur lengkap asli sebagai input, tanpa tergantung pada hasil antara.


20

The jawaban diterima bekerja dengan baik dalam khas kasus , tetapi gagal di tepi kasus , yaitu:

  • Untuk nama file tanpa ekstensi (disebut akhiran di sisa jawaban ini), extension=${filename##*.}kembalikan nama file input daripada string kosong.
  • extension=${filename##*.}tidak termasuk inisial ., bertentangan dengan konvensi.
    • Prapelisian buta .tidak akan bekerja untuk nama file tanpa akhiran.
  • filename="${filename%.*}"akan menjadi string kosong, jika nama file input dimulai dengan .dan tidak mengandung .karakter lebih lanjut (misalnya, .bash_profile) - bertentangan dengan konvensi.

---------

Dengan demikian, kompleksitas solusi yang kuat yang mencakup semua kasus tepi memerlukan fungsi - lihat definisi di bawah ini; itu dapat mengembalikan semua komponen jalan .

Contoh panggilan:

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

Perhatikan bahwa argumen setelah jalur input dipilih secara bebas, nama variabel posisional .
Untuk melewati variabel yang tidak menarik yang datang sebelum itu, tentukan _(untuk menggunakan variabel yang dibuang $_) atau ''; mis., untuk mengekstrak akar nama file dan ekstensi saja, gunakan 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

Kode uji yang menjalankan fungsi:

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

Output yang diharapkan - perhatikan kasus tepi:

  • nama file yang tidak memiliki akhiran
  • nama file dimulai dengan .( tidak dianggap sebagai akhiran akhiran)
  • jalur input yang diakhiri /(trailing /diabaikan)
  • jalur input yang hanya nama file ( .dikembalikan sebagai jalur induk)
  • nama file yang memiliki lebih dari .-prefixed token (hanya yang terakhir dianggap suffix):
----- /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

Solusi terkecil dan paling sederhana (dalam satu baris) adalah:

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

Itu penggunaan yang tidak bergunaecho . Secara umum, echo $(command)lebih baik ditulis secara sederhana commandkecuali Anda secara spesifik memerlukan shell untuk melakukan tokenization whitespace dan ekspansi wildcard pada output dari commandsebelum menampilkan hasilnya. Kuis: apa hasilnya echo $(echo '*')(dan jika itu yang benar-benar Anda inginkan, Anda benar-benar hanya menginginkan echo *).
tripleee

@ Triplee Saya tidak menggunakan echoperintah sama sekali. Saya hanya menggunakannya untuk menunjukkan hasil fooyang muncul di baris ke-3 sebagai hasil dari baris ke-2.
Ron

Tetapi hanya basename "${file%.*}"akan melakukan hal yang sama; Anda menggunakan substitusi perintah untuk menangkap outputnya, hanya untuk echooutput yang sama segera. (Tanpa mengutip, hasilnya secara nominal berbeda; tapi itu hampir tidak relevan, apalagi fitur, di sini.)
tripleee

Juga basename "$file" .txtmenghindari kerumitan substitusi parameter.
tripleee

1
@Ron Baca komentar pertamanya sebelum menuduhnya membuang-buang waktu kita.
frederick99

14

Saya pikir jika Anda hanya perlu nama file, Anda dapat mencoba ini:

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"

Dan itu semua = D.


Hanya ingin BASEDIRECTORY :) Terima kasih!
Carlos Ricardo

12

Anda dapat memaksa memotong untuk menampilkan semua bidang dan yang berikutnya menambahkan -ke nomor bidang.

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

Jadi jika FILE adalah eth0.pcap.gz, EXTENSION akan menjadipcap.gz

Dengan menggunakan logika yang sama, Anda juga dapat mengambil nama file menggunakan '-' dengan memotong sebagai berikut:

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

Ini berfungsi bahkan untuk nama file yang tidak memiliki ekstensi.


8

Pengenalan file ajaib

Selain banyak jawaban bagus untuk pertanyaan Stack Overflow ini, saya ingin menambahkan:

Di Linux dan unixen lainnya, ada perintah ajaib bernama file, yang melakukan deteksi tipe file dengan menganalisis beberapa byte pertama file. Ini adalah alat yang sangat lama, awalnya digunakan untuk server cetak (jika tidak dibuat untuk ... Saya tidak yakin tentang itu).

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

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

Ekstensi standar dapat ditemukan di /etc/mime.types(di desktop Debian GNU / Linux. Lihat man filedan man mime.types. Mungkin Anda harus menginstal fileutilitas dan mime-supportpaket):

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

Anda dapat membuat berfungsi untuk menentukan ekstensi yang tepat. Ada sedikit (tidak sempurna) sampel:

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

Fungsi ini dapat mengatur variabel Bash yang dapat digunakan nanti:

(Ini terinspirasi dari jawaban benar @Petesh):

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

echo "$fullfile -> $filename . $extension"

8

Ok jadi jika saya mengerti dengan benar, masalahnya di sini adalah bagaimana mendapatkan nama dan ekstensi penuh dari file yang memiliki banyak ekstensi, misalnya stuff.tar.gz,.

Ini bekerja untuk saya:

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

Ini akan memberi Anda stuffnama file dan .tar.gzekstensi. Ini berfungsi untuk sejumlah ekstensi, termasuk 0. Semoga ini membantu bagi siapa pun yang memiliki masalah yang sama =)


Hasil yang benar (sesuai dengan os.path.splitext, yang diinginkan OP) adalah ('stuff.tar', '.gz').
Cyker

6

Saya menggunakan skrip berikut

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

Ini sama sekali tidak efisien. Untuk mem-fork terlalu banyak kali yang sangat tidak perlu karena operasi ini dapat dilakukan dalam Bash murni tanpa perlu perintah eksternal dan forking.
codeforester

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

Ini melayani beberapa titik dan spasi dalam nama file, namun jika tidak ada ekstensi itu mengembalikan nama file itu sendiri. Mudah untuk diperiksa; hanya menguji nama file dan ekstensi menjadi sama.

Tentu metode ini tidak berfungsi untuk file .tar.gz. Namun itu bisa ditangani dalam proses dua langkah. Jika ekstensi gz maka periksa lagi untuk melihat apakah ada juga ekstensi tar.


5

Cara mengekstrak nama file dan ekstensi pada ikan :

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

Peringatan: Membagi pada titik terakhir, yang bekerja dengan baik untuk nama file dengan titik-titik di dalamnya, tetapi tidak baik untuk ekstensi dengan titik-titik di dalamnya. Lihat contoh di bawah ini.

Pemakaian:

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

Mungkin ada cara yang lebih baik untuk melakukan ini. Silakan mengedit jawaban saya untuk memperbaikinya.


Jika ada set ekstensi terbatas yang akan Anda hadapi dan Anda tahu semuanya, coba ini:

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

Ini tidak memiliki peringatan sebagai contoh pertama, tetapi Anda harus menangani setiap kasus sehingga bisa lebih membosankan tergantung pada berapa banyak ekstensi yang Anda harapkan.


4

Berikut ini adalah kode dengan AWK . Itu bisa dilakukan dengan lebih sederhana. Tapi saya tidak pandai dalam 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

Anda seharusnya tidak membutuhkan pernyataan awk pertama dalam contoh terakhir, bukan?
BHSPitMonkey

Anda dapat menghindari perpipaan Awk ke Awk dengan melakukan yang lain split(). awk -F / '{ n=split($2, a, "."); print a[n] }' uses / `sebagai pembatas tingkat atas tetapi kemudian membelah bidang kedua .dan mencetak elemen terakhir dari array baru.
tripleee

4

Cukup gunakan ${parameter%word}

Dalam kasus Anda:

${FILE%.*}

Jika Anda ingin mengujinya, semua pekerjaan berikut, dan cukup hapus ekstensi:

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

2
Mengapa downvote? Ini masih berguna, meskipun seharusnya tidak ada ruang di sekitar =tanda - tanda.
SilverWolf - Reinstate Monica

1
Ini berfungsi dengan baik. Terima kasih! (sekarang tidak memiliki ruang di sekitar tanda-tanda yang sama, jika itu sebabnya itu diturunkan)
Alex. S.

3

Membangun dari jawaban Petesh , jika hanya nama file yang dibutuhkan, kedua jalur dan ekstensi dapat dilucuti dalam satu baris,

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

Tidak berfungsi untuk saya: "basename: missing operand Coba 'basename --help' untuk informasi lebih lanjut."
Helmy

Aneh, apakah Anda yakin menggunakan Bash? Dalam kasus saya, dengan kedua versi 3.2.25 (CentOS lama) dan 4.3.30 (Debian Jessie) berfungsi dengan sempurna.
cvr

Mungkin ada spasi di nama file? Coba gunakanfilename="$(basename "${fullname%.*}")"
Adrian

Argumen kedua basenameadalah opsional, tetapi menentukan ekstensi untuk dihapus. Substitusi mungkin masih berguna tetapi mungkin basenamesebenarnya tidak, karena Anda dapat benar-benar melakukan semua penggantian ini dengan shell bawaan.
tripleee

3

Sebagian besar didasarkan off dari sangat baik @ mklement0 ini, dan penuh sesak acak, berguna bashisms - serta jawaban lain untuk ini / pertanyaan lain / "yang sialan internet" ... aku membungkus semuanya dalam sedikit, sedikit lebih dipahami, fungsi yang dapat digunakan kembali untuk saya (atau Anda) .bash_profileyang mengurus apa (saya anggap) harus menjadi versi yang lebih kuat dari dirname/ basename/ apa yang sudah Anda ..

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
}     

Contoh penggunaan ...

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
Bagus sekali; beberapa saran: - Anda tampaknya tidak bergantung $IFSsama sekali (dan jika ya, Anda dapat menggunakan localuntuk melokalisasi efek pengaturannya). - Lebih baik menggunakan localvariabel. - Pesan kesalahan Anda harus di-output stderr, bukan stdout(digunakan 1>&2), dan Anda harus mengembalikan kode keluar yang tidak nol. - Lebih baik untuk mengubah nama fullnamemenjadi basename(yang pertama menyarankan jalur dengan komponen dir). - nametanpa syarat menambahkan .(periode), bahkan jika aslinya tidak punya. Anda bisa menggunakan basenameutilitas, tetapi perhatikan bahwa ia mengabaikan terminating /.
mklement0

2

Jawaban sederhana:

Untuk memperluas jawaban variabel POSIX , perhatikan bahwa Anda dapat melakukan pola yang lebih menarik. Jadi untuk kasus yang dirinci di sini, Anda bisa melakukan ini:

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

Itu akan memotong kejadian terakhir .tar. <sesuatu> .

Lebih umum, jika Anda ingin menghapus kejadian terakhir. <sesuatu> . <sesuatu-selain> lalu

${1.*.*}

harus bekerja dengan baik.

Tautan jawaban di atas tampaknya sudah mati. Berikut adalah penjelasan yang bagus tentang sekelompok manipulasi string yang dapat Anda lakukan langsung di Bash, dari TLDP .


Apakah ada cara untuk membuat pertandingan tidak peka huruf besar-kecil?
tonix

2

Jika Anda juga ingin mengizinkan ekstensi kosong , ini adalah yang terpendek yang dapat saya buat:

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

Baris pertama menjelaskan: Ini cocok dengan PATH.EXT atau APA SAJA dan menggantikannya dengan EXT. Jika APA SAJA dicocokkan, grup ext tidak ditangkap.


2

Ini adalah satu-satunya yang bekerja untuk saya:

path='folder/other_folder/file.js'

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

>> file

Ini juga dapat digunakan dalam interpolasi string, tetapi sayangnya Anda harus mengatur baseterlebih dahulu.


1

Berikut adalah algoritma yang saya gunakan untuk menemukan nama dan ekstensi file ketika saya menulis skrip Bash untuk membuat nama-nama unik ketika nama-nama bertentangan dengan sehubungan dengan casing.

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

Uji coba.

$ 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: Program transliterasi lengkap dan lebih banyak test case dapat ditemukan di sini: https://www.dropbox.com/s/4c6m0f2e28a1vxf/avoid-clashes-code.zip?dl=0


Dari semua solusi, ini adalah satu-satunya yang mengembalikan string kosong ketika file tidak memiliki ekstensi dengan:extension=$([[ "$theFileName" == *.* ]] && echo ".${theFileName##*.}" || echo '')
f0nzie

1

Menggunakan file contoh /Users/Jonathan/Scripts/bash/MyScript.sh, kode ini:

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

akan menghasilkan ${ME}menjadi MyScriptdan ${MY_EXT}menjadi .sh:


Naskah:

#!/bin/bash
set -e

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

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

Beberapa tes:

$ ./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
Tidak yakin mengapa ini memiliki banyak downvotes - sebenarnya lebih efisien daripada jawaban yang diterima. (Sebagai yang terakhir, itu juga rusak dengan input nama file tanpa ekstensi). Menggunakan jalur eksplisit untuk basename, mungkin, berlebihan.
mklement0

1

Dari jawaban di atas, oneliner terpendek ke meniru Python

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

anggap file Anda benar-benar memiliki ekstensi, adalah

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

Saya punya banyak pendapat tentang ini. Saya sedang mempertimbangkan untuk menghapus jawabannya, orang-orang entah bagaimana tidak menyukainya.
commonpike

basename tidak menghapus ekstensi, hanya path.
David Cullen

Sudah begitu lama sejak saya melihat halaman manual saya lupa tentang opsi SUFFIX.
David Cullen

Anda harus tahu ekstensi mana yang ingin Anda lepas sebelum Anda tahu apa yang harus dimasukkan EXTjadi ini adalah kura-kura sepanjang jalan. (Juga, Anda harus menghindari semua huruf besar untuk nama variabel pribadi Anda; mereka dicadangkan untuk variabel sistem.)
tripleee
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.