Extraire le nom de fichier et l'extension dans Bash

2113

Je veux obtenir le nom de fichier (sans extension) et l'extension séparément.

La meilleure solution que j'ai trouvée jusqu'à présent est:

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

C'est faux car cela ne fonctionne pas si le nom de fichier contient plusieurs .caractères. Si, disons, je l'ai a.b.js, il considérera aet b.js, au lieu de a.bet js.

Cela peut être facilement fait en Python avec

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

mais je préfère ne pas lancer un interpréteur Python juste pour cela, si possible.

De meilleures idées?

ibz
la source
Cette question explique cette technique de bash et plusieurs autres techniques connexes.
jjclarkson
28
Lorsque vous appliquez les bonnes réponses ci-dessous, ne collez pas simplement votre variable comme je le montre ici Mauvais: extension="{$filename##*.}" comme je l'ai fait pendant un certain temps! Déplacez l' $extérieur des curlys: Droite: extension="${filename##*.}"
Chris K
4
Il s'agit clairement d'un problème non trivial et pour moi, il est difficile de dire si les réponses ci-dessous sont complètement correctes. Il est étonnant que ce ne soit pas une opération intégrée dans (ba) sh (les réponses semblent implémenter la fonction en utilisant la correspondance de modèles). J'ai décidé d'utiliser Python os.path.splitextcomme ci-dessus à la place ...
Peter Gibson
1
Comme l' extension doit représenter la nature d'un fichier, il existe une commande magique qui vérifie le fichier pour deviner sa nature et propose une extension standard . voir ma réponse
F. Hauri
2
La question est problématique en premier lieu car .. Du point de vue des systèmes de fichiers OS et Unix en général, il n'y a pas d'extension de fichier. Utilisant un "." séparer les parties est une convention humaine , qui ne fonctionne que tant que les humains acceptent de la suivre. Par exemple, avec le programme «tar», il aurait pu être décidé de nommer les fichiers de sortie avec un «tar». préfixe au lieu d'un suffixe ".tar" - Donner "tar.somedir" au lieu de "somedir.tar". Il n'y a pas de solution "générale, fonctionne toujours" à cause de cela - vous devez écrire du code qui correspond à vos besoins spécifiques et aux noms de fichiers attendus.
CM

Réponses:

3507

Tout d'abord, obtenez le nom du fichier sans le chemin:

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

Alternativement, vous pouvez vous concentrer sur le dernier '/' du chemin au lieu du '.' qui devrait fonctionner même si vous avez des extensions de fichier imprévisibles:

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

Vous voudrez peut-être vérifier la documentation:

Petesh
la source
85
Consultez gnu.org/software/bash/manual/html_node/… pour l'ensemble des fonctionnalités.
D.Shawley
24
Ajoutez des guillemets à "$ fullfile", sinon vous risquez de casser le nom de fichier.
lhunath
47
Heck, vous pourriez même écrire filename = "$ {fullfile ## * /}" et éviter d'appeler un extrabasename
éphémère
45
Cette "solution" ne fonctionne pas si le fichier n'a pas d'extension - au lieu de cela, le nom de fichier entier est sorti, ce qui est assez mauvais étant donné que les fichiers sans extensions sont omniprésents.
nccc
43
Fix pour traiter les noms de fichiers sans extension: extension=$([[ "$filename" = *.* ]] && echo ".${filename##*.}" || echo ''). Notez que si une extension est présente, elle sera retournée, y compris l'initiale ., par exemple .txt.
mklement0
685
~% FILE="example.tar.gz"

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

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

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

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

Pour plus de détails, voir Extension des paramètres du shell dans le manuel de Bash.

Juliano
la source
22
Vous (peut-être involontairement) soulève l'excellente question de savoir quoi faire si la partie "extension" du nom de fichier contient 2 points, comme dans .tar.gz ... Je n'ai jamais considéré ce problème, et je soupçonne que c'est non résoluble sans connaître à l'avance toutes les extensions de fichier valides possibles.
rmeador
8
Pourquoi pas résoluble? Dans mon exemple, il faut considérer que le fichier contient deux extensions, pas une extension à deux points. Vous gérez les deux extensions séparément.
Juliano
22
Il est insoluble sur une base lexicale, vous devrez vérifier le type de fichier. Considérez si vous aviez un jeu appelé dinosaurs.in.taret que vous l'avez compressé pour dinosaurs.in.tar.gz:)
porges
11
Cela devient plus compliqué si vous passez par des chemins complets. L'un des miens avait un '.' dans un répertoire au milieu du chemin, mais aucun dans le nom du fichier. Exemple "a / bc / d / e / nom de fichier" se terminerait par ".c / d / e / nom de fichier"
Walt Sellers
7
il est clair que x.tar.gzl'extension n'est pas gzet le nom de fichier est x.tarcelui-ci. Il n'y a pas de double extension. je suis presque sûr que boost :: filesystem le gère de cette façon. (split path, change_extension ...) et son comportement est basé sur python si je ne me trompe pas.
v.oddou
432

Habituellement, vous connaissez déjà l'extension, vous pouvez donc utiliser:

basename filename .extension

par exemple:

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

et nous obtenons

filename
Tomi Po
la source
61
Ce deuxième argument basenameest tout à fait
révélateur
10
Et comment extraire l'extension, en utilisant cette technique? ;) Oh, attendez! En fait, nous ne le savons pas d'avance.
Tomasz Gandor
3
Supposons que vous ayez un répertoire compressé qui se termine par .zipou .ZIP. Y a-t-il un moyen de faire quelque chose comme ça basename $file {.zip,.ZIP}?
Dennis
8
Bien que cela ne réponde qu'à une partie de la question OP, cela répond à la question que j'ai tapée dans google. :-) Très lisse!
sudo make install
1
facile et conforme POSIX
gpanda
147

Vous pouvez utiliser la magie de l'expansion des paramètres POSIX:

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

Il y a une mise en garde: si votre nom de fichier était de la forme, ./somefile.tar.gzalors echo ${FILENAME%%.*}supprimerait avidement la correspondance la plus longue avec le .et vous auriez la chaîne vide.

(Vous pouvez contourner cela avec une variable temporaire:

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

)


Ce site explique plus.

${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
la source
5
Beaucoup plus simple que la réponse de Joachim mais je dois toujours rechercher la substitution de variable POSIX. En outre, cela fonctionne sur Max OSX où il cutn'a pas --complementet sedn'a pas -r.
jwadsack
72

Cela ne semble pas fonctionner si le fichier n'a pas d'extension ou pas de nom de fichier. Voici ce que j'utilise; il utilise uniquement des fonctions internes et gère plus (mais pas tous) les noms de fichiers pathologiques.

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

Et voici quelques testcases:

$ 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 / moi /:
    dir = "/ home / me /"
    base = ""
    ext = ""
/ home / moi / fichier:
    dir = "/ home / me /"
    base = "fichier"
    ext = ""
/home/me/file.tar:
    dir = "/ home / me /"
    base = "fichier"
    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 / moi / ..:
    dir = "/ home / me /"
    base = ".."
    ext = ""
.:
    dir = ""
    base = "."
    ext = ""
Docteur J
la source
2
Au lieu de dir="${fullpath:0:${#fullpath} - ${#filename}}"je l'ai souvent vu dir="${fullpath%$filename}". C'est plus simple à écrire. Je ne sais pas s'il y a une réelle différence de vitesse ou des accrochages.
dubiousjim
2
Cela utilise #! / Bin / bash, ce qui est presque toujours faux. Préférez #! / Bin / sh si possible ou #! / Usr / bin / env bash sinon.
Bonne personne
@Good Person: Je ne sais pas comment c'est presque toujours faux: which bash-> /bin/bash; c'est peut-être votre distribution?
vol7ron
2
@ vol7ron - sur de nombreuses distributions, bash se trouve dans / usr / local / bin / bash. Sur OSX, de nombreuses personnes installent un bash mis à jour dans / opt / local / bin / bash. En tant que tel / bin / bash est faux et il faut utiliser env pour le trouver. Encore mieux est d'utiliser les constructions / bin / sh et POSIX. Sauf sur Solaris, il s'agit d'un shell POSIX.
Good Person
2
@GoodPerson mais si vous êtes plus à l'aise avec bash, pourquoi utiliser sh? N'est-ce pas comme dire: pourquoi utiliser Perl quand on peut utiliser sh?
vol7ron
46

Vous pouvez utiliser basename.

Exemple:

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

Vous devez fournir le nom de base avec l'extension à supprimer, mais si vous exécutez toujours taravec, -zvous savez que l'extension le sera .tar.gz.

Cela devrait faire ce que vous voulez:

tar -zxvf $1
cd $(basename $1 .tar.gz)
Bjarke Freund-Hansen
la source
2
Je suppose que cela cd $(basename $1 .tar.gz)fonctionne pour les fichiers .gz. Mais en question, il a mentionnéArchive files have several extensions: tar.gz, tat.xz, tar.bz2
SS Hegde
Tomi Po a posté les mêmes choses 2 ans auparavant.
phil294
Salut Blauhirn, wauw c'est une vieille question. Je pense que quelque chose est arrivé aux dates. Je me souviens distinctement avoir répondu à la question peu de temps après qu'elle a été posée, et là où seulement quelques autres réponses. Se pourrait-il que la question ait été fusionnée avec une autre, est-ce que SO le fait?
Bjarke Freund-Hansen
Oui, je me souviens bien. J'ai à l'origine répondu à cette question stackoverflow.com/questions/14703318/… le même jour où elle a été posée, 2 ans plus tard, elle a été fusionnée dans celle-ci. Je peux difficilement être blâmé pour une réponse en double lorsque ma réponse a été déplacée de cette façon.
Bjarke Freund-Hansen
38
pax> echo a.b.js | sed 's/\.[^.]*$//'
a.b
pax> echo a.b.js | sed 's/^.*\.//'
js

fonctionne bien, vous pouvez donc simplement utiliser:

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

Soit dit en passant, les commandes fonctionnent comme suit.

La commande pour NAMEsubstitue un "."caractère suivi d'un nombre quelconque de non- "."caractères jusqu'à la fin de la ligne, sans rien (c'est-à-dire qu'elle supprime tout de la finale "."à la fin de la ligne, inclus). Il s'agit essentiellement d'une substitution non gourmande utilisant la ruse regex.

La commande pour EXTENSIONsubstitue un nombre quelconque de caractères suivi d'un "."caractère au début de la ligne, sans rien (c'est-à-dire qu'elle supprime tout du début de la ligne au point final, inclus). Il s'agit d'une substitution gourmande qui est l'action par défaut.

paxdiablo
la source
Cette coupure pour les fichiers sans extension car elle s'imprimerait de la même façon pour le nom et l'extension. J'utilise donc sed 's,\.[^\.]*$,,'pour le nom et sed 's,.*\.,., ;t ;g'pour l'extension (utilise les commandes atypiques testet get, ainsi que la substitutecommande typique ).
hIpPy
32

Mellen écrit dans un commentaire sur un article de blog:

En utilisant Bash, il y a aussi ${file%.*}pour obtenir le nom de fichier sans l'extension et ${file##*.}pour obtenir l'extension seule. C'est,

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

Les sorties:

filename: thisfile
extension: txt
Kebabbert
la source
2
@REACHUS: Voir gnu.org/software/bash/manual/html_node/…
mklement0
29

Pas besoin de s'embêter avec awkou sedou même perlpour cette tâche simple. Il existe une os.path.splitext()solution entièrement compatible avec Bash qui n'utilise que des extensions de paramètres.

Implémentation de référence

Documentation de os.path.splitext(path):

Divisez le chemin d'accès en une paire (root, ext)telle que root + ext == path, et ext est vide ou commence par un point et contient au plus un point. Les premières périodes sur le nom de base sont ignorées; splitext('.cshrc')retourne ('.cshrc', '').

Code Python:

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

Implémentation de Bash

Honorer les périodes marquantes

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

Ignorer les périodes principales

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

Les tests

Voici des cas de test pour l' implémentation Ignorer les périodes principales , qui doivent correspondre à l'implémentation de référence Python sur chaque entrée.

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

Résultats de test

Tous les tests ont réussi.

Cyker
la source
2
non, le nom du fichier de base pour text.tar.gzdevrait être textet l'extension être.tar.gz
frederick99
2
@ frederick99 Comme je l'ai dit, la solution correspond ici à l'implémentation de os.path.splitextPython. La question de savoir si cette mise en œuvre est raisonnable pour des contributions éventuellement controversées est un autre sujet.
Cyker
Comment fonctionnent les guillemets dans le pattern ( "$root")? Que pourrait-il arriver s'ils étaient omis? (Je n'ai trouvé aucune documentation à ce sujet.) Comment cela gère-t-il les noms de fichiers avec *ou ?en eux?
ymett
Ok, les tests me montrent que les guillemets font du motif un littéral, c'est *-à- dire et ?ne sont pas spéciaux. Les deux parties de ma question se répondent donc. Ai-je raison de dire que cela n'est pas documenté? Ou est-ce censé être compris du fait que les guillemets désactivent l'expansion globale en général?
ymett
Réponse brillante! Je vais juste suggérer une variante légèrement plus simple pour calculer la racine: root="${path#?}";root="${path::1}${root%.*}"- puis procédez de même pour extraire l'extension.
Maëlan
26

Vous pouvez utiliser la cutcommande pour supprimer les deux dernières extensions (la ".tar.gz"partie):

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

Comme l'a noté Clayton Hughes dans un commentaire, cela ne fonctionnera pas pour l'exemple réel de la question. Donc, comme alternative, je propose d'utiliser seddes expressions régulières étendues, comme ceci:

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

Il fonctionne en supprimant inconditionnellement les deux dernières extensions (alphanumériques).

[Mis à jour à nouveau après le commentaire d'Anders Lindahl]

Un mec programmeur
la source
4
Cela ne fonctionne que dans le cas où le nom de fichier / chemin ne contient aucun autre point: echo "mpc-1.0.1.tar.gz" | couper -d '.' --complement -f2- produit "mpc-1" (juste les 2 premiers champs après la délimitation par.)
Clayton Hughes
@ClaytonHughes Vous avez raison, et j'aurais dû mieux le tester. Ajout d'une autre solution.
Un programmeur mec
Les expressions sed doivent utiliser $pour vérifier que l'extension correspondante se trouve à la fin du nom de fichier. Sinon, un nom de fichier comme i.like.tar.gz.files.tar.bz2pourrait produire un résultat inattendu.
Anders Lindahl
@AndersLindahl Il en sera toujours ainsi si l'ordre des extensions est l'inverse de l' sedordre des chaînes. Même avec $à la fin un nom de fichier tel que mpc-1.0.1.tar.bz2.tar.gzva supprimer les deux .tar.gzet ensuite .tar.bz2.
Un programmeur du
$ echo "foo.tar.gz" | couper -d '.' -f2- SANS --complement obtiendra le 2ème élément divisé à la fin de la chaîne $ echo "foo.tar.gz" | couper -d '.' -f2- tar.gz
Gene Black
23

Voici quelques suggestions alternatives (principalement en awk), y compris des cas d'utilisation avancés, comme l'extraction des numéros de version pour les packages logiciels.

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

Tous les cas d'utilisation utilisent le chemin complet d'origine comme entrée, sans dépendre des résultats intermédiaires.

henfiber
la source
20

La réponse acceptée fonctionne bien dans les typiques cas , mais échoue bord des cas , à savoir:

  • Pour les noms de fichiers sans extension (appelé suffixe dans le reste de cette réponse), extension=${filename##*.}renvoie le nom de fichier d'entrée plutôt qu'une chaîne vide.
  • extension=${filename##*.}ne comprend pas l'initiale ., contrairement à la convention.
    • Le préfixe aveugle .ne fonctionnerait pas pour les noms de fichiers sans suffixe.
  • filename="${filename%.*}"sera la chaîne vide, si le nom du fichier d'entrée commence par .et ne contient aucun autre .caractère (par exemple, .bash_profile) - contrairement à la convention.

---------

Ainsi, la complexité d'une solution robuste qui couvre tous les cas marginaux appelle une fonction - voir sa définition ci-dessous; il peut renvoyer tous les composants d'un chemin .

Exemple d'appel:

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

Notez que les arguments après le chemin d'entrée sont des noms de variables de position librement choisis .
Pour ignorer les variables sans intérêt qui précèdent celles qui le sont, spécifiez _(pour utiliser la variable à jeter $_) ou ''; par exemple, pour extraire uniquement la racine et l'extension du nom de fichier, utilisez 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

Code de test qui exerce la fonction:

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

Sortie attendue - notez les cas de bord:

  • un nom de fichier sans suffixe
  • un nom de fichier commençant par .( non considéré comme le début du suffixe)
  • un chemin d'entrée se terminant par /(fin/ est ignorée)
  • un chemin d'entrée qui est uniquement un nom de fichier ( .est renvoyé comme chemin parent)
  • un nom de fichier qui a plus d' .un jeton préfixé (seul le dernier est considéré comme le suffixe):
----- /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
la source
19

La solution la plus petite et la plus simple (en une seule ligne) est:

$ file=/blaabla/bla/blah/foo.txt
echo $(basename ${file%.*}) # foo
Ron
la source
C'est une utilisation inutile deecho . En général, il echo $(command)vaut mieux écrire simplement, commandsauf si vous avez spécifiquement besoin du shell pour effectuer une tokenisation des espaces blancs et une expansion générique sur la sortie commandavant d'afficher le résultat. Quiz: quelle est la sortie de echo $(echo '*')(et si c'est ce que vous voulez vraiment, vous voulez vraiment vraiment juste echo *).
tripleee
@triplee Je n'ai pas utilisé de echocommande du tout. Je viens de l'utiliser pour démontrer le résultat fooqui apparaît sur la 3e ligne comme résultat de la 2e ligne.
Ron
Mais basename "${file%.*}"ferait la même chose; vous utilisez une substitution de commande pour capturer sa sortie, uniquement sur echocette même sortie immédiatement. (Sans citer, le résultat est nominalement différent; mais ce n'est guère pertinent, encore moins une fonctionnalité, ici.)
tripleee
basename "$file" .txtEvite également la complexité de la substitution de paramètres.
tripleee
1
@Ron Lisez son premier commentaire avant de l'accuser de perdre notre temps.
frederick99
14

Je pense que si vous avez juste besoin du nom du fichier, vous pouvez essayer ceci:

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"

Et c'est tout = D.

Andrew Woolfgang
la source
Je voulais juste BASEDIRECTORY :) Merci!
Carlos Ricardo
12

Vous pouvez forcer la coupure pour afficher tous les champs et les suivants en ajoutant -au numéro de champ.

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

Donc, si FILE est eth0.pcap.gz, l'extension serapcap.gz

En utilisant la même logique, vous pouvez également récupérer le nom de fichier en utilisant '-' avec cut comme suit:

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

Cela fonctionne même pour les noms de fichiers sans extension.

maciek gajewski
la source
8

Reconnaissance magique des fichiers

En plus des nombreuses bonnes réponses à cette question sur le débordement de pile, je voudrais ajouter:

Sous Linux et autres Unixen, il existe une commande magique nommée file, qui fait la détection de type de fichier en analysant les premiers octets de fichier. Il s'agit d'un outil très ancien, initialement utilisé pour les serveurs d'impression (s'il n'est pas créé pour ... je n'en suis pas sûr).

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

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

Des extensions de standards peuvent être trouvées dans /etc/mime.types(sur mon bureau Debian GNU / Linux. Voir man fileet man mime.types. Peut-être devez-vous installer l' fileutilitaire et les mime-supportpaquets):

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

Vous pouvez créer un fonction pour déterminer l'extension droite. Il y a un petit échantillon (pas parfait):

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

Cette fonction pourrait définir une variable Bash pouvant être utilisée ultérieurement:

(Ceci est inspiré de la bonne réponse @Petesh):

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

echo "$fullfile -> $filename . $extension"
F. Hauri
la source
8

Ok donc si je comprends bien, le problème ici est de savoir comment obtenir le nom et l'extension complète d'un fichier qui a plusieurs extensions, par exemple stuff.tar.gz.

Cela fonctionne pour moi:

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

Cela vous donnera stuffcomme nom de fichier et .tar.gzcomme extension. Cela fonctionne pour n'importe quel nombre d'extensions, y compris 0. J'espère que cela aide pour toute personne ayant le même problème =)

Al3xXx
la source
Le résultat correct (selon os.path.splitext, qui est ce que le PO souhaite) est ('stuff.tar', '.gz').
Cyker
6

J'utilise le script suivant

$ echo "foo.tar.gz"|rev|cut -d"." -f3-|rev
foo
Joydip Datta
la source
Ce n'est pas du tout efficace. Forks trop de fois ce qui est tout à fait inutile puisque cette opération peut être effectuée en pur Bash sans avoir besoin de commandes externes et de forking.
codeforester
5
$ F = "text file.test.txt"  
$ echo ${F/*./}  
txt  

Cela prend en charge plusieurs points et espaces dans un nom de fichier, mais s'il n'y a pas d'extension, il renvoie le nom de fichier lui-même. Facile à vérifier cependant; testez simplement que le nom de fichier et l'extension sont identiques.

Naturellement, cette méthode ne fonctionne pas pour les fichiers .tar.gz. Cependant, cela pourrait être géré en deux étapes. Si l'extension est gz, vérifiez à nouveau pour voir s'il existe également une extension tar.

Miriam English
la source
5

Comment extraire le nom de fichier et l'extension dans le poisson :

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

Avertissements: fractionnements sur le dernier point, ce qui fonctionne bien pour les noms de fichiers contenant des points, mais pas bien pour les extensions contenant des points. Voir l'exemple ci-dessous.

Usage:

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

Il y a probablement de meilleures façons de le faire. N'hésitez pas à modifier ma réponse pour l'améliorer.


S'il y a un ensemble limité d'extensions que vous allez traiter et que vous les connaissez toutes, essayez ceci:

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

Cela n'a pas la mise en garde comme premier exemple, mais vous devez gérer chaque cas, donc cela pourrait être plus fastidieux en fonction du nombre d'extensions que vous pouvez attendre.

Dennis
la source
4

Voici le code avec AWK . Cela peut être fait plus simplement. Mais je ne suis pas bon en 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
la source
Vous ne devriez pas avoir besoin de la première instruction awk dans le dernier exemple, non?
BHSPitMonkey
Vous pouvez éviter de diriger Awk vers Awk en faisant une autre split(). awk -F / '{ n=split($2, a, "."); print a[n] }' uses / `comme délimiteur de niveau supérieur, mais divise ensuite les seconds champs .et imprime le dernier élément du nouveau tableau.
tripleee
4

Utilisez simplement ${parameter%word}

Dans ton cas:

${FILE%.*}

Si vous voulez le tester, tous les travaux suivants, et supprimez simplement l'extension:

FILE=abc.xyz; echo ${FILE%.*};
FILE=123.abc.xyz; echo ${FILE%.*};
FILE=abc; echo ${FILE%.*};
enyo
la source
2
Pourquoi le downvote? C'est toujours utile, bien qu'il ne devrait pas y avoir d'espace autour des =panneaux.
SilverWolf
1
Cela fonctionne bien. Je vous remercie! (maintenant il n'a pas les espaces autour des signes égaux, si c'est pourquoi il a été rétrogradé)
Alex. S.
3

Construire à partir de la réponse Petesh , si seul le nom de fichier est nécessaire, le chemin et l'extension peuvent être supprimés sur une seule ligne,

filename=$(basename ${fullname%.*})
cvr
la source
N'a pas fonctionné pour moi: "nom de base: opérande manquant Essayez" nom de base - aide "pour plus d'informations."
helmy
Étrange, êtes-vous certain que vous utilisez Bash? Dans mon cas, avec les deux versions 3.2.25 (ancien CentOS) et 4.3.30 (Debian Jessie), cela fonctionne parfaitement.
cvr
Peut-être qu'il y a un espace dans le nom de fichier? Essayez d'utiliserfilename="$(basename "${fullname%.*}")"
Adrian
Le deuxième argument de basenameest facultatif, mais spécifie l'extension à supprimer. La substitution peut toujours être utile, mais peut-être pas basenamevraiment, car vous pouvez réellement effectuer toutes ces substitutions avec les commandes internes du shell.
tripleee
3

Basé en grande partie sur l'excellent et le plein de bashismes aléatoires et utiles de @ mklement0 - ainsi que d'autres réponses à cette / d'autres questions / "ce sacrément Internet" ... J'ai résumé le tout dans un petit peu, fonction réutilisable pour mon (ou votre) .bash_profilequi s'occupe de ce que (je considère) devrait être une version plus robuste de dirname/ basename/ ce que vous avez ..

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
}     

Exemples d'utilisation ...

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
la source
1
Bien fait; quelques suggestions: - Vous ne semblez pas compter du $IFStout (et si vous l'étiez, vous pourriez utiliser localpour localiser l'effet du réglage). - Mieux vaut utiliser des localvariables. - Votre message d'erreur doit être envoyé à stderr, pas stdout(utiliser 1>&2) et vous devez renvoyer un code de sortie différent de zéro. - Il vaut mieux renommer fullnameà basename(l'ancien chemin suggère un des composants dir). - nameajoute inconditionnellement un .(point), même si l'original n'en avait pas. Vous pouvez simplement utiliser l' basenameutilitaire, mais notez qu'il ignore une terminaison /.
mklement0
2

Une réponse simple:

Pour développer la réponse des variables POSIX , notez que vous pouvez faire des modèles plus intéressants. Donc, pour le cas détaillé ici, vous pouvez simplement faire ceci:

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

Cela coupera la dernière occurrence de .tar. <quelque chose> .

Plus généralement, si vous souhaitez supprimer la dernière occurrence de. <quelque chose> . <something-else> alors

${1.*.*}

devrait bien fonctionner.

Le lien de la réponse ci-dessus semble être mort. Voici une excellente explication d'un tas de la manipulation de chaînes que vous pouvez faire directement dans Bash, à partir de TLDP .

RandyP
la source
Existe-t-il un moyen de rendre la correspondance insensible à la casse?
tonix
2

Si vous souhaitez également autoriser les extensions vides , c'est la plus courte que j'ai pu trouver:

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

1ère ligne expliquée: elle correspond à PATH.EXT ou à TOUT et la remplace par EXT. Si N'IMPORTE QUOI correspond, le groupe ext n'est pas capturé.

phil294
la source
2

C'est le seul qui a fonctionné pour moi:

path='folder/other_folder/file.js'

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

>> file

Cela peut également être utilisé dans l'interpolation de chaînes, mais malheureusement, vous devez le définir baseau préalable.

Ken Mueller
la source
1

Voici l'algorithme que j'ai utilisé pour trouver le nom et l'extension d'un fichier lorsque j'ai écrit un script Bash pour rendre les noms uniques en cas de conflit de noms par rapport à la casse.

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

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

FYI: Le programme complet de translittération et plus de cas de test peuvent être trouvés ici: https://www.dropbox.com/s/4c6m0f2e28a1vxf/avoid-clashes-code.zip?dl=0

historystamp
la source
De toutes les solutions, c'est la seule qui retourne une chaîne vide lorsque le fichier n'a pas d'extension avec:extension=$([[ "$theFileName" == *.* ]] && echo ".${theFileName##*.}" || echo '')
f0nzie
1

En utilisant un fichier d'exemple /Users/Jonathan/Scripts/bash/MyScript.sh, ce code:

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

se traduira par ${ME}être MyScriptet ${MY_EXT}être .sh:


Scénario:

#!/bin/bash
set -e

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

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

Quelques tests:

$ ./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
la source
2
Je ne sais pas pourquoi cela a autant de votes négatifs - c'est en fait plus efficace que la réponse acceptée. (Comme ce dernier, il rompt également avec les noms de fichiers d'entrée sans extension). L'utilisation d'un chemin explicite vers basenameest peut-être exagéré.
mklement0
1

D'après les réponses ci-dessus, le plus court oneliner pour imiter Python

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

en supposant que votre fichier a vraiment une extension, est

EXT="${PATH##*.}"; FILE=$(basename "$PATH" .$EXT)
pic commun
la source
J'ai des votes négatifs à ce sujet. J'envisage de supprimer la réponse, les gens ne l'aiment pas.
commonpike
nom de base ne supprime pas l'extension, juste le chemin.
David Cullen
Cela fait si longtemps que je n'ai pas regardé la page de manuel que j'ai oublié l'option SUFFIX.
David Cullen
Vous devez savoir quelle extension vous souhaitez EXTsupprimer avant de savoir quoi mettre, donc c'est des tortues tout le long. (De plus, vous devez éviter tout en majuscule pour vos noms de variables privées; ils sont réservés aux variables système.)
tripleee