Extraer nombre de archivo y extensión en Bash

2111

Quiero obtener el nombre de archivo (sin extensión) y la extensión por separado.

La mejor solución que encontré hasta ahora es:

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

Esto está mal porque no funciona si el nombre del archivo contiene varios .caracteres. Si, digamos, tengo a.b.js, considerará ay b.js, en lugar de a.by js.

Se puede hacer fácilmente en Python con

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

pero preferiría no encender un intérprete de Python solo por esto, si es posible.

¿Alguna idea mejor?

ibz
fuente
Esta pregunta explica esta técnica de bash y varias otras relacionadas.
jjclarkson
28
Cuando aplique las excelentes respuestas a continuación, no pegue simplemente su variable como lo muestro aquí Incorrecto: ¡ extension="{$filename##*.}" como lo hice por un tiempo! Mover el $exterior de los rizos: Derecha: extension="${filename##*.}"
Chris K
44
Este es claramente un problema no trivial y para mí es difícil saber si las respuestas a continuación son completamente correctas. Es sorprendente que esta no sea una operación integrada en (ba) sh (las respuestas parecen implementar la función usando la coincidencia de patrones). Decidí usar Python os.path.splitextcomo el anterior ...
Peter Gibson
1
Como la extensión tiene que representar la naturaleza de un archivo, hay un comando mágico que verifica el archivo para adivinar su naturaleza y ofrece una extensión estándar . mira mi respuesta
F. Hauri
2
La pregunta es problemática en primer lugar porque ... Desde la perspectiva del sistema operativo y los sistemas de archivos Unix en general, no existe una extensión de archivo. Usando un "." separar partes es una convención humana , que solo funciona mientras los humanos acuerden seguirla. Por ejemplo, con el programa 'tar', podría haberse decidido nombrar los archivos de salida con un "tar". prefijo en lugar de un sufijo ".tar" - Dar "tar.somedir" en lugar de "somedir.tar". No existe una solución "general, siempre funciona" debido a esto: debe escribir código que coincida con sus necesidades específicas y los nombres de archivo esperados.
CM

Respuestas:

3504

Primero, obtenga el nombre del archivo sin la ruta:

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

Alternativamente, puede centrarse en el último '/' de la ruta en lugar del '.' que debería funcionar incluso si tiene extensiones de archivo impredecibles:

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

Es posible que desee consultar la documentación:

Petesh
fuente
85
Consulte gnu.org/software/bash/manual/html_node/… para ver el conjunto completo de funciones.
D.Shawley
24
Agregue algunas comillas a "$ fullfile", o correrá el riesgo de romper el nombre del archivo.
lhunath
47
Diablos, incluso podría escribir filename = "$ {fullfile ## * /}" y evitar llamar a un extrabasename
ephemient
45
Esta "solución" no funciona si el archivo no tiene una extensión; en su lugar, se muestra el nombre completo del archivo, lo cual es bastante malo teniendo en cuenta que los archivos sin extensiones son omnipresentes.
nccc
43
Arreglo para tratar con nombres de archivo sin extensión: extension=$([[ "$filename" = *.* ]] && echo ".${filename##*.}" || echo ''). Tenga en cuenta que si una extensión es presente, se le devolverá incluyendo la inicial ., por ejemplo, .txt.
mklement0
684
~% FILE="example.tar.gz"

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

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

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

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

Para obtener más detalles, consulte la expansión de parámetros de shell en el manual de Bash.

Juliano
fuente
22
Usted (tal vez involuntariamente) plantea la excelente pregunta de qué hacer si la parte de "extensión" del nombre de archivo tiene 2 puntos, como en .tar.gz ... Nunca he considerado ese problema, y ​​sospecho que es no se puede resolver sin conocer todas las posibles extensiones de archivo válidas por adelantado.
rmeador
8
¿Por qué no solucionable? En mi ejemplo, debería considerarse que el archivo contiene dos extensiones, no una extensión con dos puntos. Manejas ambas extensiones por separado.
Juliano
22
No se puede resolver sobre una base léxica, deberá verificar el tipo de archivo. Considere si usted tenía un juego llamado dinosaurs.in.tary gzipped a dinosaurs.in.tar.gz:)
PORGES
11
Esto se vuelve más complicado si está pasando por caminos completos. Uno de los míos tenía un '.' en un directorio en el medio de la ruta, pero ninguno en el nombre del archivo. El ejemplo "a / bc / d / e / filename" terminaría ".c / d / e / filename"
Walt Sellers
66
claramente ninguna x.tar.gzextensión es gzy el nombre de archivo es x.tareso. No existen las extensiones duales. Estoy bastante seguro de que boost :: filesystem lo maneja de esa manera. (ruta dividida, change_extension ...) y su comportamiento se basa en python si no me equivoco.
v.oddou
431

Por lo general, ya conoce la extensión, por lo que es posible que desee utilizar:

basename filename .extension

por ejemplo:

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

y obtenemos

filename
Tomi Po
fuente
61
Ese segundo argumento basenamees bastante revelador, tipo amable señor / señora :)
akaIDIOT
10
¿Y cómo extraer la extensión, utilizando esta técnica? ;) ¡Oh espera! En realidad no lo sabemos por adelantado.
Tomasz Gandor
3
Digamos que tiene un directorio comprimido que termina con .zipo .ZIP. ¿Hay alguna manera de hacer algo así basename $file {.zip,.ZIP}?
Dennis
8
Si bien esto solo responde parte de la pregunta de los OP, sí responde la pregunta que escribí en google. :-) Muy hábil!
sudo make install
1
fácil y compatible con POSIX
gpanda
147

Puede usar la magia de la expansión de parámetros POSIX:

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

Hay una advertencia de que si su nombre era de la forma ./somefile.tar.gza continuación, echo ${FILENAME%%.*}eliminaría con avidez el partido más largo de la .y que tendría la cadena vacía.

(Puede solucionar eso con una variable temporal:

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

)


Este sitio explica más.

${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
fuente
55
Mucho más simple que la respuesta de Joachim, pero siempre tengo que buscar la sustitución de variables POSIX. Además, esto se ejecuta en Max OSX donde cutno tiene --complementy sedno tiene -r.
jwadsack
72

Eso no parece funcionar si el archivo no tiene extensión o no tiene nombre de archivo. Esto es lo que estoy usando; solo usa builtins y maneja más (pero no todos) nombres de archivos patológicos.

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

Y aquí hay algunos casos de prueba:

$ 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 = ""
/casa/:
    dir = "/ inicio / yo /"
    base = ""
    ext = ""
/ home / me / file:
    dir = "/ inicio / yo /"
    base = "archivo"
    ext = ""
/home/me/file.tar:
    dir = "/ inicio / yo /"
    base = "archivo"
    ext = "alquitrán"
/home/me/file.tar.gz:
    dir = "/ inicio / yo /"
    base = "file.tar"
    ext = "gz"
/home/me/.hidden:
    dir = "/ inicio / yo /"
    base = ".hidden"
    ext = ""
/home/me/.hidden.tar:
    dir = "/ inicio / yo /"
    base = ".hidden"
    ext = "alquitrán"
/casa/..:
    dir = "/ inicio / yo /"
    base = ".."
    ext = ""
.:
    dir = ""
    base = "."
    ext = ""
Doctor j
fuente
2
En lugar de lo dir="${fullpath:0:${#fullpath} - ${#filename}}"que he visto a menudo dir="${fullpath%$filename}". Es más sencillo de escribir. No estoy seguro de si hay alguna diferencia de velocidad real o problemas.
dubiousjim
2
Esto usa #! / Bin / bash que casi siempre está mal. Prefiera #! / Bin / sh si es posible o #! / Usr / bin / env bash si no es así.
Buena persona
@Buena Persona: No sé cómo casi siempre está mal: which bash-> /bin/bash; tal vez es tu distro?
vol7ron
2
@ vol7ron: en muchas distribuciones, bash está en / usr / local / bin / bash. En OSX, muchas personas instalan un bash actualizado en / opt / local / bin / bash. Como tal, / bin / bash está mal y uno debe usar env para encontrarlo. Aún mejor es usar / bin / sh y las construcciones POSIX. Excepto en Solaris, este es un shell POSIX.
Buena persona
2
@GoodPerson, pero si te sientes más cómodo con bash, ¿por qué usar sh? ¿No es eso como decir, por qué usar Perl cuando puedes usar sh?
vol7ron
46

Puedes usar basename.

Ejemplo:

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

Debe proporcionar el nombre base con la extensión que se eliminará, sin embargo, si siempre está ejecutando tarcon, -zentonces sabe que la extensión será .tar.gz.

Esto debería hacer lo que quieras:

tar -zxvf $1
cd $(basename $1 .tar.gz)
Bjarke Freund-Hansen
fuente
2
Supongo que cd $(basename $1 .tar.gz)funciona para archivos .gz. Pero en cuestión mencionóArchive files have several extensions: tar.gz, tat.xz, tar.bz2
SS Hegde
Tomi Po publicó las mismas cosas 2 años antes.
phil294
Hola Blauhirn, wauw esta es una vieja pregunta. Creo que algo le ha pasado a las fechas. Recuerdo claramente haber respondido la pregunta poco después de haberla formulado, y allí solo había un par de otras respuestas. ¿Podría ser que la pregunta se fusionó con otra, SO hace eso?
Bjarke Freund-Hansen
Sí, lo recuerdo correctamente. Originalmente respondí a esta pregunta stackoverflow.com/questions/14703318/... el mismo día en que se hizo, 2 años después se fusionó con esta. Difícilmente se me puede culpar por una respuesta duplicada cuando mi respuesta se movió de esta manera.
Bjarke Freund-Hansen
38
pax> echo a.b.js | sed 's/\.[^.]*$//'
a.b
pax> echo a.b.js | sed 's/^.*\.//'
js

funciona bien, así que puedes usar:

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

Los comandos, por cierto, funcionan de la siguiente manera.

El comando para NAMEsustituye un "."carácter seguido de cualquier número de no "."caracteres hasta el final de la línea, sin nada (es decir, elimina todo desde el final "."hasta el final de la línea, inclusive). Esto es básicamente una sustitución no codiciosa que usa trucos regex.

El comando para EXTENSIONsustituye cualquier número de caracteres seguido de un "."carácter al comienzo de la línea, sin nada (es decir, elimina todo desde el inicio de la línea hasta el punto final, inclusive). Esta es una sustitución codiciosa, que es la acción predeterminada.

paxdiablo
fuente
Este salto para archivos sin extensión ya que imprimiría lo mismo para nombre y extensión. Entonces uso sed 's,\.[^\.]*$,,'para nombre y sed 's,.*\.,., ;t ;g'para extensión (usa el atípico testy los getcomandos, junto con el substitutecomando típico ).
FELIZ
32

Mellen escribe en un comentario en una publicación de blog:

Usando Bash, también hay ${file%.*}que obtener el nombre de archivo sin la extensión y ${file##*.}obtener solo la extensión. Es decir,

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

Salidas:

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

No hay necesidad de molestarse con awko sedo incluso perlpara esta tarea simple. Hay un puro Bash,os.path.splitext() solución compatible con que solo utiliza expansiones de parámetros.

Implementación de referencia

Documentación de os.path.splitext(path):

Dividir la ruta de nombre de ruta en un par (root, ext)de tal manera que root + ext == path, y ext está vacío o comienza con un período y contiene como máximo un periodo. Los períodos iniciales en el nombre base se ignoran; splitext('.cshrc')vuelve ('.cshrc', '').

Código de Python:

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

Implementación de Bash

Honrando los períodos principales

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

Ignorando los períodos principales

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

Pruebas

Aquí hay casos de prueba para la implementación de ignorar los períodos iniciales, que deben coincidir con la implementación de referencia de Python en cada entrada.

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

Resultados de la prueba

Todas las pruebas pasaron.

Cyker
fuente
2
no, el nombre del archivo base text.tar.gzdebe ser texty la extensión ser.tar.gz
frederick99
2
@ frederick99 Como dije, la solución aquí coincide con la implementación de os.path.splitextPython. Si esa implementación es sensata para posibles aportaciones controvertidas es otro tema.
Cyker
¿Cómo funcionan las comillas dentro del patrón ( "$root")? ¿Qué podría pasar si se omitieran? (No pude encontrar ninguna documentación al respecto). ¿Cómo maneja esto los nombres de archivo con *o ?en ellos?
ymett
Ok, las pruebas me muestran que las citas hacen que el patrón sea literal, es decir, *y ?no son especiales. Entonces las dos partes de mi pregunta se responden entre sí. ¿Estoy en lo cierto de que esto no está documentado? ¿O se supone que esto debe entenderse por el hecho de que las citas deshabilitan la expansión global en general?
ymett
Brillante respuesta! Solo sugeriré una variante ligeramente más simple para calcular la raíz: root="${path#?}";root="${path::1}${root%.*}"- luego proceda de la misma manera para extraer la extensión.
Maëlan
26

Puede usar el cutcomando para eliminar las dos últimas extensiones (la ".tar.gz"parte):

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

Como señaló Clayton Hughes en un comentario, esto no funcionará para el ejemplo real en la pregunta. Entonces, como alternativa, propongo usar sedexpresiones regulares extendidas, como esta:

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

Funciona eliminando las dos últimas extensiones (alfanuméricas) incondicionalmente.

[Actualizado nuevamente después del comentario de Anders Lindahl]

Algún tipo programador
fuente
44
Esto solo funciona en el caso de que el nombre de archivo / ruta no contenga ningún otro punto: echo "mpc-1.0.1.tar.gz" | cortar -d '.' --complement -f2- produce "mpc-1" (solo los primeros 2 campos después de delimitar).
Clayton Hughes
@ClaytonHughes Tienes razón, y debería haberlo probado mejor. Se agregó otra solución.
Algún tipo programador el
Las expresiones sed deben usarse $para verificar que la extensión coincidente esté al final del nombre del archivo. De lo contrario, un nombre de archivo como i.like.tar.gz.files.tar.bz2podría producir un resultado inesperado.
Anders Lindahl
@AndersLindahl Todavía lo hará, si el orden de las extensiones es el inverso del sedorden de la cadena. Incluso con $al final un nombre de archivo como mpc-1.0.1.tar.bz2.tar.gzeliminará ambos .tar.gzy luego .tar.bz2.
Algún tipo programador el
$ echo "foo.tar.gz" | cortar -d '.' -f2- SIN --complemento obtendrá el segundo elemento dividido al final de la cadena $ echo "foo.tar.gz" | cortar -d '.' -f2- tar.gz
Gene Black
23

Aquí hay algunas sugerencias alternativas (principalmente en awk), incluidos algunos casos de uso avanzados, como extraer números de versión para paquetes de 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 '^[/]\|^~/'

Todos los casos de uso utilizan la ruta completa original como entrada, sin depender de resultados intermedios.

gallina
fuente
20

La respuesta aceptada funciona bien en típicos casos , pero no en los bordes de los casos , a saber:

  • Para nombres de archivo sin extensión (llamado sufijo en el resto de esta respuesta), extension=${filename##*.}devuelve el nombre de archivo de entrada en lugar de una cadena vacía.
  • extension=${filename##*.}no incluye la inicial ., contrario a la convención.
    • Pretender a ciegas .no funcionaría para nombres de archivo sin sufijo.
  • filename="${filename%.*}"será la cadena vacía, si el nombre del archivo de entrada comienza con .y no contiene más .caracteres (p. ej. .bash_profile), contrario a la convención.

---------

Por lo tanto, la complejidad de una solución robusta que cubre todos los casos extremos requiere una función : consulte su definición a continuación; se puede volver todos los componentes de un camino .

Llamada de ejemplo:

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

Tenga en cuenta que los argumentos después de la ruta de entrada se eligen libremente, nombres de variables posicionales .
Para omitir variables que no son de interés anteriores a las que sí lo son, especifique _(para usar la variable desechable $_) o ''; por ejemplo, para extraer la raíz y la extensión del nombre de archivo solamente, use 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

Código de prueba que ejerce la función:

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

Resultado esperado: tenga en cuenta los casos límite:

  • un nombre de archivo que no tiene sufijo
  • un nombre de archivo que comienza con .( no considera el inicio del sufijo)
  • una ruta de entrada que termina en /(final/ se ignora el )
  • una ruta de entrada que es solo un nombre de archivo ( .se devuelve como la ruta principal)
  • un nombre de archivo que tiene más de .un token prefijado (solo el último se considera el sufijo):
----- /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
fuente
19

La solución más pequeña y más simple (en una sola línea) es:

$ file=/blaabla/bla/blah/foo.txt
echo $(basename ${file%.*}) # foo
Ron
fuente
Ese es un uso inútil deecho . En general, echo $(command)está mejor escrito simplemente a commandmenos que requiera específicamente que el shell realice la tokenización de espacios en blanco y la expansión de comodines en la salida desde commandantes de mostrar el resultado. Prueba: ¿cuál es el resultado de echo $(echo '*')(y si eso es lo que realmente quieres, realmente realmente solo quieres echo *).
tripleee
@triplee No utilicé el echocomando en absoluto. Lo acabo de usar para demostrar el resultado fooque aparece en la tercera línea como resultado de la segunda línea.
Ron
Pero solo basename "${file%.*}"haría lo mismo; está utilizando una sustitución de comando para capturar su salida, solo a echoesa misma salida de inmediato. (Sin citar, el resultado es nominalmente diferente; pero eso no es relevante, mucho menos una característica, aquí)
tripleee
también basename "$file" .txt evita la complejidad de la sustitución de parámetros.
tripleee
1
@Ron Lee su primer comentario antes de acusarlo de perder el tiempo.
frederick99
14

Creo que si solo necesita el nombre del archivo, puede intentar esto:

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"

Y eso es todo = D.

Andrew Woolfgang
fuente
Solo quería BASEDIRECTORY :) ¡Gracias!
Carlos Ricardo
12

Puede forzar el corte para mostrar todos los campos y los siguientes que se agregan -al número de campo.

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

Entonces, si ARCHIVO es eth0.pcap.gz , la EXTENSIÓN serápcap.gz

Usando la misma lógica, también puede buscar el nombre del archivo usando '-' con el corte de la siguiente manera:

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

Esto funciona incluso para nombres de archivo que no tienen ninguna extensión.

maciek gajewski
fuente
8

Reconocimiento de archivos mágicos

Además de la gran cantidad de buenas respuestas sobre esta pregunta de desbordamiento de pila, me gustaría agregar:

Bajo Linux y otros Unixen, hay un comando mágico llamado file, que detecta el tipo de archivo al analizar algunos primeros bytes del archivo. Esta es una herramienta muy antigua, utilizada inicialmente para servidores de impresión (si no se creó para ... no estoy seguro de eso).

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

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

Se pueden encontrar extensiones de estándares en /etc/mime.types(en mi escritorio Debian GNU / Linux. Ver man filey man mime.types. Quizás tenga que instalar la fileutilidad y los mime-supportpaquetes):

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

Podrías crear un función para determinar la extensión correcta. Hay una pequeña muestra (no perfecta):

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

Esta función podría establecer una variable Bash que se pueda usar más tarde:

(Esto está inspirado en la respuesta correcta de @Petesh):

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

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

Ok por lo que si he entendido bien, el problema aquí es cómo conseguir que el nombre y la extensión completa de un archivo que tiene múltiples extensiones, por ejemplo, stuff.tar.gz.

Esto funciona para mi:

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

Esto le dará stuffcomo nombre de archivo y .tar.gzcomo extensión. Funciona para cualquier cantidad de extensiones, incluyendo 0. Espero que esto ayude a cualquiera que tenga el mismo problema =)

Al3xXx
fuente
El resultado correcto (según os.path.splitext, que es lo que quiere el OP) es ('stuff.tar', '.gz').
Cyker
6

Yo uso el siguiente script

$ echo "foo.tar.gz"|rev|cut -d"." -f3-|rev
foo
Joydip Datta
fuente
Esto no es eficiente en absoluto. Bifurcar demasiadas veces, lo cual es bastante innecesario ya que esta operación se puede realizar en Bash puro sin la necesidad de comandos externos y bifurcación.
codeforester
5
$ F = "text file.test.txt"  
$ echo ${F/*./}  
txt  

Esto atiende a múltiples puntos y espacios en un nombre de archivo, sin embargo, si no hay extensión, devuelve el nombre de archivo en sí. Aunque es fácil de verificar; solo pruebe que el nombre de archivo y la extensión sean los mismos.

Naturalmente, este método no funciona para archivos .tar.gz. Sin embargo, eso podría manejarse en un proceso de dos pasos. Si la extensión es gz, verifique nuevamente para ver si también hay una extensión tar.

Miriam English
fuente
5

Cómo extraer el nombre de archivo y la extensión en peces :

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

Advertencias: se divide en el último punto, que funciona bien para nombres de archivo con puntos, pero no para extensiones con puntos. Ver ejemplo a continuación.

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.

Probablemente hay mejores formas de hacer esto. Siéntase libre de editar mi respuesta para mejorarla.


Si hay un conjunto limitado de extensiones con las que se enfrentará y las conoce todas, intente esto:

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

Esto no tiene la advertencia como primer ejemplo, pero tiene que manejar cada caso, por lo que podría ser más tedioso dependiendo de cuántas extensiones pueda esperar.

Dennis
fuente
4

Aquí hay un código con AWK . Se puede hacer de manera más simple. Pero no soy bueno 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
cara sonriente
fuente
No debería necesitar la primera declaración awk en el último ejemplo, ¿verdad?
BHSPitMonkey
Puede evitar conectar Awk a Awk haciendo otro split(). awk -F / '{ n=split($2, a, "."); print a[n] }' uses / `como delimitador de nivel superior, pero luego divide los segundos campos .e imprime el último elemento de la nueva matriz.
tripleee
4

Simplemente use ${parameter%word}

En tu caso:

${FILE%.*}

Si quieres probarlo, todo lo siguiente funciona y simplemente elimina la extensión:

FILE=abc.xyz; echo ${FILE%.*};
FILE=123.abc.xyz; echo ${FILE%.*};
FILE=abc; echo ${FILE%.*};
enyo
fuente
2
¿Por qué el voto negativo? Sigue siendo útil, aunque no debería haber espacios alrededor de los =letreros.
SilverWolf - Restablece a Monica
1
Esto funciona bien ¡Gracias! (ahora no tiene los espacios alrededor de los signos iguales, si por eso fue rechazado)
Alex. S.
3

A partir de la respuesta de Petesh , si solo se necesita el nombre de archivo, tanto la ruta como la extensión se pueden quitar en una sola línea,

filename=$(basename ${fullname%.*})
cvr
fuente
No funcionó para mí: "basename: falta el operando Intente 'basename --help' para obtener más información".
helmy
Extraño, ¿estás seguro de que estás usando Bash? En mi caso, con ambas versiones 3.2.25 (antiguo CentOS) y 4.3.30 (Debian Jessie) funciona perfectamente.
cvr
Tal vez hay un espacio en el nombre del archivo? Intente usarfilename="$(basename "${fullname%.*}")"
Adrian
El segundo argumento basenamees opcional, pero especifica la extensión que se va a quitar. La sustitución aún puede ser útil, pero tal vez en basenamerealidad no lo sea, ya que en realidad puede realizar todas estas sustituciones con cascarillas incorporadas.
tripleee
3

Basado en gran medida en los excelentes bashismos de @ mklement0 y repletos de bashismos útiles y aleatorios, así como otras respuestas a esta / otras preguntas / "ese maldito internet" ... Lo resumí todo en un poco, un poco más comprensible, función reutilizable para mi (o tu) .bash_profileque se encarga de lo que (considero) debería ser una versión más robusta de dirname/ basename/ lo que tienes ..

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
}     

Ejemplos de uso ...

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
fuente
1
Bien hecho; algunas sugerencias: - Parece que no confía $IFSen absoluto (y si lo fuera, podría usarlo localpara localizar el efecto de configurarlo). - Mejor usar localvariables. - Su mensaje de error debe enviarse a stderr, no stdout(usar 1>&2), y debe devolver un código de salida distinto de cero. - Es mejor cambiar el nombre fullnamea basename(el primero sugiere una ruta con componentes dir). - nameagrega incondicionalmente un .(punto), incluso si el original no tenía ninguno. Simplemente puede usar la basenameutilidad, pero tenga en cuenta que ignora una terminación /.
mklement0
2

Una respuesta simple:

Para ampliar la respuesta de las variables POSIX , tenga en cuenta que puede hacer patrones más interesantes. Entonces, para el caso detallado aquí, simplemente puede hacer esto:

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

Eso cortará la última aparición de .tar. <algo> .

En términos más generales, si desea eliminar la última aparición de. <algo> . <algo más> entonces

${1.*.*}

Debería funcionar bien.

El enlace de la respuesta anterior parece estar muerto. Aquí hay una gran explicación de un montón de manipulación de cadenas que puede hacer directamente en Bash, desde TLDP .

RandyP
fuente
¿Hay alguna manera de hacer que el partido no distinga entre mayúsculas y minúsculas?
tonix
2

Si también desea permitir extensiones vacías , esto es lo más corto que se me ocurre:

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

Primera línea explicada: coincide con PATH.EXT o CUALQUIER COSA y lo reemplaza con EXT. Si CUALQUIER COSA coincide, el grupo ext no se captura.

phil294
fuente
2

Este es el único que funcionó para mí:

path='folder/other_folder/file.js'

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

>> file

Esto también se puede usar en la interpolación de cadenas, pero desafortunadamente debe configurarlo de baseantemano.

Ken Mueller
fuente
1

Aquí está el algoritmo que utilicé para encontrar el nombre y la extensión de un archivo cuando escribí un script Bash para hacer que los nombres sean únicos cuando los nombres entran en conflicto con respecto a la carcasa.

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

La prueba de funcionamiento.

$ 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: El programa completo de transliteración y más casos de prueba se pueden encontrar aquí: https://www.dropbox.com/s/4c6m0f2e28a1vxf/avoid-clashes-code.zip?dl=0

sello histórico
fuente
De todas las soluciones, esta es la única que devuelve una cadena vacía cuando el archivo no tiene extensión con:extension=$([[ "$theFileName" == *.* ]] && echo ".${theFileName##*.}" || echo '')
f0nzie
1

Usando el archivo de ejemplo /Users/Jonathan/Scripts/bash/MyScript.sh, este código:

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

resultará en ${ME}ser MyScripty ${MY_EXT}ser .sh:


Guión:

#!/bin/bash
set -e

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

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

Algunas pruebas:

$ ./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
fuente
2
No estoy seguro de por qué esto tiene tantos votos negativos: en realidad es más eficiente que la respuesta aceptada. (Como este último, también se rompe con los nombres de archivo de entrada sin una extensión). Usar una ruta explícita a basenamees, quizás, exagerado.
mklement0
1

De las respuestas anteriores, la línea más corta para imitar a Python

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

suponiendo que su archivo realmente tenga una extensión, es

EXT="${PATH##*.}"; FILE=$(basename "$PATH" .$EXT)
Pico común
fuente
Tengo votos negativos sobre esto. Estoy considerando eliminar la respuesta, a la gente de alguna manera no le gusta.
commonpike
basename no elimina la extensión, solo la ruta.
David Cullen
Ha pasado tanto tiempo desde que miré la página de manual que olvidé la opción SUFFIX.
David Cullen
Tienes que saber qué extensión quieres quitar antes de saber qué poner EXTpara que se trate de tortugas hasta el fondo. (Además, debe evitar todo en mayúsculas para sus nombres de variables privadas; están reservados para las variables del sistema.)
tripleee