Estrai il nome del file e l'estensione in Bash


2110

Voglio ottenere il nome file (senza estensione) e l'estensione separatamente.

La migliore soluzione che ho trovato finora è:

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

Questo è sbagliato perché non funziona se il nome del file contiene più .caratteri. Se, diciamo, ho a.b.js, prenderà in considerazione ae b.js, invece di a.be js.

Può essere facilmente eseguito in Python con

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

ma preferirei non accendere un interprete Python solo per questo, se possibile.

Qualche idea migliore?


Questa domanda spiega questa tecnica bash e molte altre correlate.
jjclarkson,

28
Quando applichi le grandi risposte di seguito, non incollare semplicemente la tua variabile come mostro qui Sbagliato: extension="{$filename##*.}" come ho fatto per un po '! Sposta l' $esterno dei curlys: A destra: extension="${filename##*.}"
Chris K,

4
Questo è chiaramente un problema non banale e per me è difficile dire se le risposte di seguito sono completamente corrette. È sorprendente che questa non sia un'operazione integrata in (ba) sh (le risposte sembrano implementare la funzione usando il pattern matching). Ho deciso di usare Python os.path.splitextcome sopra invece ...
Peter Gibson,

1
Poiché l' estensione deve rappresentare la natura di un file, esiste un comando magico che controlla il file per divinarne la natura e offrire l'estensione standard . vedi la mia risposta
F. Hauri,

2
La domanda è problematica in primo luogo perché .. Dal punto di vista del sistema operativo e dei file system unix in generale, non esiste un'estensione di file. Usare un "." separare le parti è una convenzione umana , che funziona solo finché gli umani accettano di seguirla. Ad esempio, con il programma 'tar', si sarebbe potuto decidere di nominare i file di output con un "tar". prefisso invece del suffisso ".tar" - Dando "tar.somedir" invece di "somedir.tar". Non esiste una soluzione "generale, funziona sempre" per questo - devi scrivere codice che corrisponda alle tue esigenze specifiche e ai nomi dei file previsti.
CM

Risposte:


3504

Innanzitutto, ottieni il nome del file senza il percorso:

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

In alternativa, puoi concentrarti sull'ultimo "/" del percorso anziché su "." che dovrebbe funzionare anche se hai estensioni di file imprevedibili:

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

Potresti voler controllare la documentazione:


85
Controlla gnu.org/software/bash/manual/html_node/… per il set completo di funzionalità.
D.Shawley,

24
Aggiungi alcune virgolette a "$ fullfile", altrimenti rischi di spezzare il nome del file.
lhunath,

47
Cavolo, potresti persino scrivere il nome del file = "$ {fullfile ## * /}" ed evitare di chiamare un extrabasename
effimero

45
Questa "soluzione" non funziona se il file non ha un'estensione - invece, viene emesso l'intero nome del file, il che è abbastanza male considerando che i file senza estensioni sono onnipresenti.
nccc,

43
Fix per trattare con i nomi di file senza estensione: extension=$([[ "$filename" = *.* ]] && echo ".${filename##*.}" || echo ''). Se è presente un'estensione , questa verrà restituita includendo l'iniziale ., ad es .txt.
mklement0

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

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

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

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

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

Per maggiori dettagli, consultare l' espansione dei parametri della shell nel manuale di Bash.


22
Tu (forse involontariamente) sollevi l'eccellente domanda su cosa fare se la parte "extension" del nome del file contiene 2 punti, come in .tar.gz ... Non ho mai considerato questo problema e sospetto che sia non risolvibile senza conoscere tutte le possibili estensioni di file valide in anticipo.
rmeador,

8
Perché non risolvibile? Nel mio esempio, si dovrebbe considerare che il file contiene due estensioni, non un'estensione con due punti. Gestisci entrambe le estensioni separatamente.
Juliano,

22
È irrisolvibile su base lessicale, è necessario controllare il tipo di file. Considera se hai avuto un gioco chiamato dinosaurs.in.tare lo hai decompresso in dinosaurs.in.tar.gz:)
porges il

11
Questo diventa più complicato se stai attraversando percorsi completi. Uno dei miei aveva un "." in una directory nel mezzo del percorso, ma nessuna nel nome del file. Esempio "a / bc / d / e / nomefile" verrebbe visualizzato ".c / d / e / nomefile"
Walt Sellers

6
chiaramente x.tar.gzl'estensione non esiste gze il nome file è x.tarquello. Non esistono estensioni doppie. sono abbastanza sicuro boost :: filesystem lo gestisce in questo modo. (split path, change_extension ...) e il suo comportamento si basa su Python se non sbaglio.
v.

431

Di solito conosci già l'estensione, quindi potresti voler usare:

basename filename .extension

per esempio:

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

e otteniamo

filename

61
Quel secondo argomento basenameè piuttosto sorprendente, signore gentile / signora :)
akaIDIOT

10
E come estrarre l'estensione, usando questa tecnica? ;) Oh, aspetta! In realtà non lo sappiamo in anticipo.
Tomasz Gandor,

3
Supponi di avere una directory zippata che termina con .zipo .ZIP. C'è un modo in cui potresti fare qualcosa del genere basename $file {.zip,.ZIP}?
Dennis,

8
Sebbene ciò risponda solo a una parte della domanda relativa ai PO, risponde alla domanda che ho digitato su google. :-) Molto lucido!
sudo make installa il

1
facile e conforme POSIX
gpanda,

147

Puoi usare la magia dell'espansione dei parametri POSIX:

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

C'è un avvertimento nel senso che se il nome del file è di forma ./somefile.tar.gzquindi echo ${FILENAME%%.*}eliminerebbe avidamente il match più lungo al .e che avresti avuto la stringa vuota.

(Puoi aggirare quello con una variabile temporanea:

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

)


Questo sito spiega di più.

${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
Molto più semplice della risposta di Joachim, ma devo sempre cercare la sostituzione delle variabili POSIX. Inoltre, questo funziona su Max OSX dove cutnon ha --complemente sednon ha -r.
jwadsack,

72

Ciò non sembra funzionare se il file non ha estensione o nome file. Ecco cosa sto usando; utilizza solo builtin e gestisce più (ma non tutti) nomi di file patologici.

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

E qui ci sono alcuni test:

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

2
Invece di quello dir="${fullpath:0:${#fullpath} - ${#filename}}"che ho visto spesso dir="${fullpath%$filename}". Scrivere è più semplice. Non sono sicuro che ci sia qualche differenza di velocità reale o gotchas.
dubiousjim,

2
Questo usa #! / Bin / bash che è quasi sempre sbagliato. Preferisci #! / Bin / sh se possibile o #! / Usr / bin / env bash in caso contrario.
Brava persona,

@Buona persona: non so come sia quasi sempre sbagliato: which bash-> /bin/bash; forse è la tua distribuzione?
vol7ron,

2
@ vol7ron - su molte distro bash è in / usr / local / bin / bash. Su OSX molte persone installano un bash aggiornato in / opt / local / bin / bash. Come tale / bin / bash è sbagliato e si dovrebbe usare env per trovarlo. Ancora meglio è usare i costrutti / bin / sh e POSIX. Ad eccezione di Solaris, si tratta di una shell POSIX.
Brava persona,

2
@GoodPerson ma se ti senti più a tuo agio con bash, perché usare sh? Non è come dire, perché usare Perl quando puoi usare sh?
vol7ron,

46

È possibile utilizzare basename.

Esempio:

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

È necessario fornire il nome di base con l'estensione che deve essere rimossa, tuttavia se si esegue sempre tarcon -zallora si sa che l'estensione sarà .tar.gz.

Questo dovrebbe fare quello che vuoi:

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

2
Suppongo cd $(basename $1 .tar.gz)che funzioni per i file .gz. Ma in questione ha citatoArchive files have several extensions: tar.gz, tat.xz, tar.bz2
SS Hegde il

Tomi Po ha pubblicato le stesse cose 2 anni prima.
phil294,

Ciao Blauhirn, wauw questa è una vecchia domanda. Penso che sia successo qualcosa alle date. Ricordo distintamente di aver risposto alla domanda poco dopo che è stata posta, e lì dove solo un paio di altre risposte. Potrebbe essere che la domanda sia stata fusa con un'altra, SO lo fa?
Bjarke Freund-Hansen,

Sì, ricordo bene. Inizialmente ho risposto a questa domanda stackoverflow.com/questions/14703318/… nello stesso giorno in cui è stata posta, 2 anni dopo è stata fusa in questa. Difficilmente posso essere incolpato di una risposta duplicata quando la mia risposta è stata spostata in questo modo.
Bjarke Freund-Hansen,

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

funziona bene, quindi puoi semplicemente usare:

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

I comandi, tra l'altro, funzionano come segue.

Il comando NAMEsostituisce un "."carattere seguito da un numero qualsiasi di non "."caratteri fino alla fine della riga, senza nulla (ovvero rimuove tutto dalla "."fine alla fine della riga, incluso). Questa è sostanzialmente una sostituzione non avida usando l'inganno regex.

Il comando EXTENSIONsostituisce un numero qualsiasi di caratteri seguito da un "."carattere all'inizio della riga, senza nulla (cioè rimuove tutto dall'inizio della riga al punto finale, compreso). Questa è una sostituzione avida che è l'azione predefinita.


Questa interruzione per i file senza estensione in quanto stampa lo stesso per nome ed estensione. Quindi io uso sed 's,\.[^\.]*$,,'per nome e sed 's,.*\.,., ;t ;g'per estensione (usa i comandi atipici teste get, insieme al substitutecomando tipico ).
hPPy

32

Mellen scrive in un commento su un post di blog:

Usando Bash, c'è anche ${file%.*}per ottenere il nome del file senza l'estensione e ${file##*.}per ottenere l'estensione da solo. Questo è,

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

Uscite:

filename: thisfile
extension: txt


29

Non c'è bisogno di perdere tempo con awko sedo anche perlper questo semplice compito. Esiste una os.path.splitext()soluzione compatibile con Pure Bash che utilizza solo espansioni di parametri.

Implementazione di riferimento

Documentazione di os.path.splitext(path):

Dividere il percorso path in una coppia (root, ext)tale che root + ext == path, ed ext è vuota o inizia con un periodo e contiene al massimo un periodo. I periodi iniziali sul nome di base vengono ignorati; splitext('.cshrc')ritorna ('.cshrc', '').

Codice Python:

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

Implementazione di Bash

Onorare i periodi principali

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

Ignorando i periodi iniziali

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

test

Ecco alcuni casi di test per l' implementazione Ignora periodi iniziali, che devono corrispondere all'implementazione di riferimento Python su ogni 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' |
|---------------|-----------|-------|

Risultati del test

Tutti i test sono passati.


2
no, il nome del file di base per text.tar.gzdovrebbe essere texted estensione essere.tar.gz
frederick99

2
@frederick99 Come ho detto, la soluzione qui corrisponde all'implementazione di os.path.splitextin Python. Se tale implementazione sia sana per input forse controversi è un altro argomento.
Cyker,

Come funzionano le virgolette all'interno del pattern ( "$root")? Cosa potrebbe accadere se fossero omessi? (Non sono riuscito a trovare documentazione sull'argomento.) In che modo gestisce i nomi dei file con *o ?in essi?
ymett,

Ok, il test mi mostra che le virgolette rendono il modello letterale, cioè *e ?non sono speciali. Quindi le due parti della mia domanda si rispondono. Ho ragione che questo non è documentato? O questo dovrebbe essere capito dal fatto che le virgolette disabilitano l'espansione glob in generale?
ymett,

Risposta brillante! Ti suggerisco solo una variante leggermente più semplice per il calcolo della radice: root="${path#?}";root="${path::1}${root%.*}"- quindi procedi allo stesso modo per estrarre l'estensione.
Maëlan,

26

È possibile utilizzare il cutcomando per rimuovere le ultime due estensioni (la ".tar.gz"parte):

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

Come notato da Clayton Hughes in un commento, questo non funzionerà per l'esempio reale nella domanda. Quindi, in alternativa, propongo di utilizzare sedcon espressioni regolari estese, in questo modo:

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

Funziona rimuovendo le ultime due estensioni (alfanumeriche) incondizionatamente.

[Aggiornato di nuovo dopo il commento di Anders Lindahl]


4
Funziona solo nel caso in cui il nome file / percorso non contenga altri punti: echo "mpc-1.0.1.tar.gz" | cut -d '.' --complement -f2- produce "mpc-1" (solo i primi 2 campi dopo la delimitazione di.)
Clayton Hughes il

@ClaytonHughes Hai ragione e avrei dovuto testarlo meglio. Aggiunta un'altra soluzione.
Qualche programmatore amico

Le espressioni sed dovrebbero usare $per verificare che l'estensione corrispondente sia alla fine del nome del file. Altrimenti, un nome di file simile i.like.tar.gz.files.tar.bz2potrebbe produrre risultati imprevisti.
Anders Lindahl il

@AndersLindahl Lo sarà ancora, se l'ordine delle estensioni è il contrario dell'ordine della sedcatena. Anche con $alla fine un nome file come mpc-1.0.1.tar.bz2.tar.gzrimuoverà entrambi .tar.gze poi .tar.bz2.
Qualcuno programmatore amico

$ echo "foo.tar.gz" | cut -d '.' -f2- WITHOUT --complement otterrà il secondo elemento diviso alla fine della stringa $ echo "foo.tar.gz" | cut -d '.' -f2- tar.gz
Gene Black

23

Ecco alcuni suggerimenti alternativi (principalmente in awk), inclusi alcuni casi d'uso avanzati, come l'estrazione dei numeri di versione per i pacchetti software.

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

Tutti i casi d'uso utilizzano come input il percorso completo originale, senza dipendere da risultati intermedi.


20

La risposta accettata funziona bene nei casi tipici , ma fallisce nei casi limite , vale a dire:

  • Per i nomi di file senza estensione (chiamato suffisso nel resto di questa risposta), extension=${filename##*.}restituisce il nome del file di input anziché una stringa vuota.
  • extension=${filename##*.}non include l'iniziale ., contrariamente alla convenzione.
    • L'anticipo cieco .non funzionerebbe con nomi di file senza suffisso.
  • filename="${filename%.*}"sarà la stringa vuota, se il nome del file di input inizia con .e non contiene altri .caratteri (ad esempio, .bash_profile), contrariamente alla convenzione.

---------

Pertanto, la complessità di una soluzione solida che copre tutti i casi limite richiede una funzione - vedere la sua definizione di seguito; si può tornare tutti i componenti di un percorso .

Esempio di chiamata:

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

Si noti che gli argomenti dopo il percorso di input sono liberamente scelti, nomi di variabili posizionali .
Per saltare le variabili non di interesse che precedono quelle che sono, specificare _(per utilizzare la variabile usa e getta $_) o ''; ad esempio, per estrarre solo la radice e l'estensione del nome file, utilizzare 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

Codice di prova che esercita la funzione:

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

Uscita prevista - notare i casi limite:

  • un nome file senza suffisso
  • un nome file che inizia con .( non considerato l'inizio del suffisso)
  • un percorso di input che termina con /(il trailing /viene ignorato)
  • un percorso di input che è solo un nome file ( .viene restituito come percorso principale)
  • un nome file che ha più di .token prefissato (solo l'ultimo è considerato il suffisso):
----- /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

La soluzione più piccola e più semplice (in linea singola) è:

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

È un uso inutile diecho . In generale, echo $(command)è meglio semplicemente scritto a commandmeno che non sia necessario che la shell esegua la tokenizzazione degli spazi bianchi e l'espansione dei caratteri jolly sull'output commandprima di visualizzare il risultato. Quiz: qual è l'output di echo $(echo '*')(e se è quello che vuoi davvero, vuoi davvero solo echo *).
Tripleee,

@triplee Non ho usato affatto il echocomando. L'ho appena usato per dimostrare il risultato fooche appare nella terza riga come risultato della seconda riga.
Ron,

Ma basename "${file%.*}"farebbe lo stesso; stai usando una sostituzione di comando per catturare il suo output, solo per echoquello stesso output immediatamente. (Senza virgolette, il risultato è nominalmente diverso; ma questo è poco rilevante, tanto meno una caratteristica, qui.)
Tripleee

basename "$file" .txtEvita anche la complessità della sostituzione dei parametri.
tripleee,

1
@Ron Leggi il suo primo commento prima di accusarlo di aver perso tempo.
frederick99,

14

Penso che se hai solo bisogno del nome del file, puoi provare questo:

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"

E questo è tutto = D.


Volevo solo BASEDIRECTORY :) Grazie!
Carlos Ricardo,

12

È possibile forzare il taglio per visualizzare tutti i campi e quelli successivi che si aggiungono -al numero del campo.

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

Quindi, se FILE lo è eth0.pcap.gz, l'estensione saràpcap.gz

Utilizzando la stessa logica, puoi anche recuperare il nome del file usando '-' con cut come segue:

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

Funziona anche con nomi di file che non hanno alcuna estensione.


8

Riconoscimento magico dei file

Oltre alle molte buone risposte su questa domanda Stack Overflow, vorrei aggiungere:

Sotto Linux e altri unixen, c'è un comando magico chiamato file, che fa il rilevamento del tipo di file analizzando alcuni primi byte di file. Questo è uno strumento molto vecchio, usato inizialmente per i server di stampa (se non creato per ... Non ne sono sicuro).

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

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

Le estensioni degli standard possono essere trovate in /etc/mime.types(sul mio desktop Debian GNU / Linux. Vedi man filee man mime.types. Forse devi installare l' fileutilità e i mime-supportpacchetti):

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

È possibile creare un funzione per determinare l'estensione corretta. C'è un piccolo esempio (non perfetto):

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

Questa funzione potrebbe impostare una variabile Bash che può essere utilizzata in seguito:

(Questo si ispira alla risposta giusta di @Petesh):

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

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

8

Ok, quindi, se ho capito bene, il problema qui è come ottenere il nome e l'estensione completa di un file che ha più estensioni, ad es stuff.tar.gz.

Questo funziona per me:

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

Questo ti darà stuffcome nome file e .tar.gzcome estensione. Funziona con qualsiasi numero di estensioni, incluso 0. Spero che questo aiuti per chiunque abbia lo stesso problema =)


Il risultato corretto (secondo os.path.splitext, che è ciò che l'OP vuole) è ('stuff.tar', '.gz').
Cyker,

6

Uso il seguente script

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

Questo non è affatto efficiente. Forchette troppe volte, il che è del tutto superfluo poiché questa operazione può essere eseguita in puro Bash senza la necessità di comandi e fork esterni.
codeforester

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

Questo si rivolge a più punti e spazi in un nome file, tuttavia se non c'è estensione restituisce il nome file stesso. Facile da verificare però; basta verificare che il nome file e l'estensione siano uguali.

Naturalmente questo metodo non funziona con i file .tar.gz. Tuttavia, ciò potrebbe essere gestito in due fasi. Se l'estensione è gz, controlla di nuovo per vedere se c'è anche un'estensione tar.


5

Come estrarre il nome file e l'estensione nei pesci :

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

Avvertenze: si divide sull'ultimo punto, che funziona bene per i nomi di file con punti all'interno, ma non bene per le estensioni con punti all'interno. Vedi esempio sotto.

Uso:

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

Probabilmente ci sono modi migliori per farlo. Sentiti libero di modificare la mia risposta per migliorarla.


Se hai a che fare con un set limitato di estensioni e le conosci tutte, prova questo:

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

Questo non ha l'avvertenza come primo esempio, ma devi gestire ogni caso in modo che possa essere più noioso a seconda di quante estensioni puoi aspettarti.


4

Ecco il codice con AWK . Può essere fatto più semplicemente. Ma non sono bravo in 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

Non dovresti aver bisogno della prima istruzione awk nell'ultimo esempio, giusto?
BHSPitMonkey il

Puoi evitare di reindirizzare Awk ad Awk facendo un altro split(). awk -F / '{ n=split($2, a, "."); print a[n] }' uses / `come delimitatore di livello superiore, ma poi divide i secondi campi .e stampa l'ultimo elemento dal nuovo array.
Tripleee,

4

Basta usare ${parameter%word}

Nel tuo caso:

${FILE%.*}

Se vuoi testarlo, tutto il seguente funziona e rimuovi l'estensione:

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

2
Perché il downvote? È ancora utile, anche se non dovrebbero esserci spazi attorno ai =segni.
SilverWolf - Ripristina Monica

1
Funziona benissimo. Grazie! (ora non ha gli spazi attorno ai segni di uguale, se questo è il motivo per cui è stato downvoted)
Alex. S.

3

Costruendo dalla risposta Petesh , se è necessario solo il nome file, sia il percorso che l'estensione possono essere eliminati in una sola riga,

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

Non ha funzionato per me: "basename: operando mancante Prova 'basename --help' per maggiori informazioni."
helmy,

Strano, sei sicuro di usare Bash? Nel mio caso, con entrambe le versioni 3.2.25 (vecchio CentOS) e 4.3.30 (Debian Jessie) funziona perfettamente.
cvr

Forse c'è uno spazio nel nome del file? Prova a utilizzarefilename="$(basename "${fullname%.*}")"
Adrian

Il secondo argomento basenameè facoltativo, ma specifica l'estensione da eliminare. La sostituzione potrebbe essere ancora utile, ma forse in basenamerealtà non lo è, poiché puoi effettivamente eseguire tutte queste sostituzioni con i comandi incorporati della shell.
Tripleee,

3

Basandomi in gran parte sull'eccellente e pieno zeppo di bashismi casuali e utili di @ mklement0 - nonché su altre risposte a questa / altre domande / "che maledettamente internet" ... ho concluso tutto in un po ', un po' più comprensibile, funzione riutilizzabile per il mio (o il tuo) .bash_profileche si occupa di ciò che (considero) dovrebbe essere una versione più solida di dirname/ basename/ cosa hai ..

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
}     

Esempi di utilizzo ...

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
Ben fatto; alcuni suggerimenti: - Sembra che non ti affidi $IFSaffatto (e se lo fossi, potresti usare localper localizzare l'effetto dell'impostazione). - Meglio usare le localvariabili. - Il messaggio di errore deve essere inviato a stderr, non stdout(utilizzare 1>&2) e si dovrebbe restituire un codice di uscita diverso da zero. - Meglio per rinominare fullnameal basename(ex suggerisce un percorso con i componenti dir). - nameaggiunge incondizionatamente un .(punto), anche se l'originale non ne aveva. È possibile semplicemente utilizzare l' basenameutilità, ma si noti che ignora una terminazione /.
mklement0,

2

Una semplice risposta:

Per espandere la risposta delle variabili POSIX , tenere presente che è possibile eseguire modelli più interessanti. Quindi, per il caso dettagliato qui, potresti semplicemente fare questo:

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

Ciò interromperà l'ultima occorrenza di .tar. <qualcosa> .

Più in generale, se si desidera rimuovere l'ultima occorrenza di. <qualcosa> . <something-else> quindi

${1.*.*}

dovrebbe funzionare bene.

Il link con la risposta sopra sembra essere morto. Ecco una grande spiegazione di un mucchio di manipolazioni di stringhe che puoi fare direttamente in Bash, da TLDP .


C'è un modo per rendere il match senza distinzione tra maiuscole e minuscole?
tonix,

2

Se vuoi anche consentire estensioni vuote , questa è la più breve che potrei trovare:

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

Spiegazione della prima riga: corrisponde a PATH.EXT o ANYTHING e lo sostituisce con EXT. Se è stato trovato ANYTHING, il gruppo ext non viene acquisito.


2

Questo è l'unico che ha funzionato per me:

path='folder/other_folder/file.js'

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

>> file

Questo può essere utilizzato anche nell'interpolazione di stringhe, ma sfortunatamente è necessario impostare in baseanticipo.


1

Ecco l'algoritmo che ho usato per trovare il nome e l'estensione di un file quando ho scritto uno script Bash per rendere i nomi univoci quando i nomi erano in conflitto rispetto al case.

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

L'esecuzione del test.

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

A proposito: il programma completo di traslitterazione e altri casi di test sono disponibili qui: https://www.dropbox.com/s/4c6m0f2e28a1vxf/avoid-clashes-code.zip?dl=0


Di tutte le soluzioni questa è l'unica che restituisce una stringa vuota quando il file non ha estensione con:extension=$([[ "$theFileName" == *.* ]] && echo ".${theFileName##*.}" || echo '')
f0nzie

1

Utilizzando il file di esempio /Users/Jonathan/Scripts/bash/MyScript.sh, questo codice:

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

si tradurrà in ${ME}essere MyScripted ${MY_EXT}essere .sh:


script:

#!/bin/bash
set -e

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

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

Alcuni test:

$ ./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
Non sono sicuro del perché questo abbia così tanti voti negativi: in realtà è più efficiente della risposta accettata. (Come quest'ultimo, si rompe anche con i nomi dei file di input senza estensione). L'uso di un percorso esplicito per basenameè, forse, eccessivo.
mklement0

1

Dalle risposte sopra, il più breve oneliner per imitare Python

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

supponendo che il tuo file abbia davvero un'estensione, lo è

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

Ho dei voti negativi su questo. Sto pensando di rimuovere la risposta, alla gente in qualche modo non piace.
commonpike,

basename non rimuove l'estensione, ma solo il percorso.
David Cullen,

È passato tanto tempo da quando ho guardato la pagina man che mi ero dimenticato dell'opzione SUFFIX.
David Cullen,

Devi sapere quale estensione vuoi rimuovere prima di sapere cosa inserire, EXTquindi si tratta di tartarughe fino in fondo. (Inoltre, dovresti evitare tutte le maiuscole per i nomi delle tue variabili private; sono riservati per le variabili di sistema.)
tripleee
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.