Extrair nome de arquivo e extensão no Bash

2110

Quero obter o nome do arquivo (sem extensão) e a extensão separadamente.

A melhor solução que encontrei até agora é:

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

Isso está errado porque não funciona se o nome do arquivo contiver vários .caracteres. Se, digamos, eu tiver a.b.js, ele considerará ae b.js, em vez de a.bejs .

Isso pode ser feito facilmente em Python com

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

mas prefiro não acionar um intérprete Python apenas para isso, se possível.

Alguma ideia melhor?

ibz
fonte
Esta pergunta explica essa técnica do bash e várias outras relacionadas.
jjclarkson
28
Ao aplicar as ótimas respostas abaixo, não basta colar sua variável como mostro aqui Errado: extension="{$filename##*.}" como fiz por um tempo! Mova o $lado de fora do curlys: Direita: extension="${filename##*.}"
Chris K
4
Este é claramente um problema não trivial e, para mim, é difícil dizer se as respostas abaixo estão completamente corretas. É incrível que isso não seja uma operação integrada em (ba) sh (as respostas parecem implementar a função usando a correspondência de padrões). Eu decidi usar Python os.path.splitextcomo acima, em vez ...
Peter Gibson
1
Como a extensão precisa representar a natureza de um arquivo, existe um comando mágico que verifica o arquivo para adivinhar sua natureza e oferecer extensão padrão . veja a minha resposta #
F. Hauri 14/10
2
A questão é problemática em primeiro lugar porque .. Da perspectiva do sistema operacional e dos sistemas de arquivos unix em geral, não existe extensão de arquivo. Usando um "." separar partes é uma convenção humana , que só funciona enquanto os humanos concordarem em segui-la. Por exemplo, com o programa 'tar', poderia ter sido decidido nomear os arquivos de saída com um "tar". prefixo em vez de um sufixo ".tar" - Fornecendo "tar.somedir" em vez de "somedir.tar". Não existe uma solução "geral, sempre funciona" por causa disso - você precisa escrever um código que corresponda às suas necessidades específicas e aos nomes de arquivos esperados.
CM

Respostas:

3504

Primeiro, obtenha o nome do arquivo sem o caminho:

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

Como alternativa, você pode focar no último '/' do caminho em vez do '.' que deve funcionar mesmo se você tiver extensões de arquivo imprevisíveis:

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

Você pode querer verificar a documentação:

Petesh
fonte
85
Confira gnu.org/software/bash/manual/html_node/… para obter o conjunto completo de recursos.
D.Shawley
24
Adicione algumas aspas a "$ fullfile", ou você corre o risco de quebrar o nome do arquivo.
lhunath
47
Caramba, você pode até escrever o nome do arquivo = "$ {fullfile ## * /}" e evitar chamar um basename
epemiente
45
Essa "solução" não funciona se o arquivo não tiver uma extensão. Em vez disso, o nome completo do arquivo é gerado, o que é bastante ruim, considerando que os arquivos sem extensões são onipresentes.
Ncc
43
Correção para lidar com nomes de arquivos sem extensão: extension=$([[ "$filename" = *.* ]] && echo ".${filename##*.}" || echo ''). Observe que, se uma extensão estiver presente, ela será retornada, incluindo a inicial ., por exemplo .txt,.
precisa
684
~% FILE="example.tar.gz"

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

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

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

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

Para mais detalhes, consulte expansão de parâmetros do shell no manual do Bash.

Juliano
fonte
22
Você (talvez não intencionalmente) levanta a excelente questão sobre o que fazer se a parte "extension" do nome do arquivo tiver 2 pontos, como em .tar.gz ... Eu nunca considerei esse problema e suspeito que seja não solucionável sem conhecer todas as possíveis extensões de arquivo válidas antecipadamente.
Rmeador
8
Por que não solucionável? No meu exemplo, deve-se considerar que o arquivo contém duas extensões, não uma extensão com dois pontos. Você lida com as duas extensões separadamente.
Juliano
22
É insolúvel em uma base lexical, você precisará verificar o tipo de arquivo. Considere se você tivesse um jogo chamado dinosaurs.in.tare você gzipped-lo para dinosaurs.in.tar.gz:)
PORGES
11
Isso fica mais complicado se você estiver passando em caminhos completos. Um dos meus tinha um '.' em um diretório no meio do caminho, mas nenhum no nome do arquivo. O exemplo "a / bc / d / e / filename" acabaria ".c / d / e / filename"
Walt Sellers
6
claramente x.tar.gza extensão de no é gze o nome do arquivo x.taré isso. Não existem extensões duplas. Eu tenho certeza que o boost :: filesystem lida dessa maneira. (caminho dividido, change_extension ...) e seu comportamento é baseado em python, se não me engano.
v.oddou
431

Normalmente você já conhece a extensão, então pode usar:

basename filename .extension

por exemplo:

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

e chegamos

filename
Tomi Po
fonte
61
Esse segundo argumento basenameé bastante revelador, tipo senhor / senhora :)
akaIDIOT
10
E como extrair a extensão, usando esta técnica? ;) Oh espere! Na verdade, não sabemos de antemão.
Tomasz Gandor
3
Digamos que você tenha um diretório compactado que termine com .zipou .ZIP. Existe uma maneira de você fazer algo assim basename $file {.zip,.ZIP}?
Dennis
8
Embora isso responda apenas parte da pergunta do OP, ele responde à pergunta que eu digitei no google. :-) Muito liso!
sudo make install
1
fácil e compatível com POSIX
gpanda
147

Você pode usar a mágica da expansão de parâmetros POSIX:

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

Há uma ressalva em que, se o seu nome de arquivo tiver o formato ./somefile.tar.gz, echo ${FILENAME%%.*}removeria avidamente a correspondência mais longa com a .string vazia.

(Você pode contornar isso com uma variável temporária:

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

)


Este site explica mais.

${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
fonte
5
Muito mais simples que a resposta de Joachim, mas eu sempre tenho que procurar a substituição de variáveis ​​POSIX. Além disso, isso é executado no Max OSX onde cutnão possui --complemente sednão possui -r.
Jwadsack
72

Isso não parece funcionar se o arquivo não tiver extensão ou nome de arquivo. Aqui está o que estou usando; ele usa apenas built-in e lida com mais (mas não todos) nomes de arquivos 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

E aqui estão alguns casos de teste:

$ 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 / eu /:
    dir = "/ home / me /"
    base = ""
    ext = ""
/ home / me / arquivo:
    dir = "/ home / me /"
    base = "arquivo"
    ext = ""
/home/me/file.tar:
    dir = "/ home / me /"
    base = "arquivo"
    ext = "alcatrão"
/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 = "alcatrão"
/ home / eu / ..:
    dir = "/ home / me /"
    base = ".."
    ext = ""
.:
    dir = ""
    base = "."
    ext = ""
Doctor J
fonte
2
Em vez de dir="${fullpath:0:${#fullpath} - ${#filename}}"eu já vi muitas vezes dir="${fullpath%$filename}". É mais simples de escrever. Não tenho certeza se existe alguma diferença de velocidade real ou truques.
dubiousjim
2
Isso usa #! / Bin / bash, o que quase sempre está errado. Prefira #! / Bin / sh, se possível, ou #! / Usr / bin / env bash, se não.
Boa pessoa
@ Good Person: Eu não sei como é quase sempre errado: which bash-> /bin/bash; talvez seja a sua distro?
precisa saber é
2
@ vol7ron - em muitas distros, o bash está em / usr / local / bin / bash. No OSX, muitas pessoas instalam um bash atualizado em / opt / local / bin / bash. Como tal / bin / bash está errado e deve-se usar o env para encontrá-lo. Ainda melhor é usar as construções / bin / sh e POSIX. Exceto no solaris, este é um shell POSIX.
Boa Pessoa
2
@ GoodPerson, mas se você está mais à vontade com o bash, por que usar sh? Não é o mesmo que dizer, por que usar Perl quando você pode usar sh?
vol7ron
46

Você pode usar basename.

Exemplo:

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

Você precisa fornecer nome base com a extensão que deve ser removido, no entanto, se você está sempre executando tarcom -zentão você sabe a extensão será .tar.gz.

Isso deve fazer o que você deseja:

tar -zxvf $1
cd $(basename $1 .tar.gz)
Bjarke Freund-Hansen
fonte
2
Suponho que cd $(basename $1 .tar.gz)funciona para arquivos .gz. Mas em questão ele mencionouArchive files have several extensions: tar.gz, tat.xz, tar.bz2
SS Hegde
Tomi Po publicou as mesmas coisas 2 anos antes.
phil294
Oi Blauhirn, wauw esta é uma pergunta antiga. Eu acho que algo aconteceu com as datas. Lembro-me distintamente de responder à pergunta logo depois que ela foi feita, e apenas mais algumas respostas. Será que a questão foi mesclada com outra, SO faz isso?
Bjarke Freund-Hansen
Sim, eu me lembro corretamente. Originalmente, respondo a esta pergunta stackoverflow.com/questions/14703318/… no mesmo dia em que foi solicitada, 2 anos depois, foi mesclada a esta. Mal posso ser responsabilizado por uma resposta duplicada quando minha resposta foi movida dessa maneira.
Bjarke Freund-Hansen
37
pax> echo a.b.js | sed 's/\.[^.]*$//'
a.b
pax> echo a.b.js | sed 's/^.*\.//'
js

funciona bem, então você pode simplesmente 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

Os comandos, a propósito, funcionam da seguinte maneira.

O comando NAMEsubstitui um "."caractere seguido por qualquer número de não- "."caracteres até o final da linha, sem nada (isto é, remove tudo do final "."ao final da linha, inclusive). Esta é basicamente uma substituição não gananciosa usando truques de expressões regulares.

O comando EXTENSIONsubstitutos substitui um número qualquer de caracteres seguido por um "."caractere no início da linha, sem nada (isto é, remove tudo do início da linha até o ponto final, inclusive). Esta é uma substituição gananciosa, que é a ação padrão.

paxdiablo
fonte
Essa quebra para arquivos sem extensão, pois imprimiria o mesmo para nome e extensão. Então, eu uso sed 's,\.[^\.]*$,,'para nome e sed 's,.*\.,., ;t ;g'extensão (usa os comandos atípicos teste get, junto com o substitutecomando típico ).
HIpPy 7/10
32

Mellen escreve em um comentário em um post do blog:

Usando o Bash, também é ${file%.*}possível obter o nome do arquivo sem a extensão e ${file##*.}obter a extensão sozinha. Isso é,

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

Saídas:

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

Não há necessidade de se preocupar com awkou sedou mesmo perlpara esta tarefa simples. Existe uma os.path.splitext()solução compatível com Bash puro que usa apenas expansões de parâmetros.

Implementação de referência

Documentação de os.path.splitext(path):

Divida o caminho do nome do caminho em um par (root, ext)tal que root + ext == path, e ext esteja vazio ou comece com um ponto e contenha no máximo um período. Os períodos iniciais no nome da base são ignorados; splitext('.cshrc')retorna ('.cshrc', '').

Código Python:

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

Implementação do Bash

Honrando os principais períodos

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

Ignorando períodos iniciais

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

Testes

Aqui estão casos de teste para a implementação de períodos iniciais Ignorando , que devem corresponder à implementação de referência do Python em 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' |
|---------------|-----------|-------|

Resultado dos testes

Todos os testes foram aprovados.

Cyker
fonte
2
não, o nome do arquivo base para text.tar.gzdeve ser texte extensão ser.tar.gz
frederick99
2
@ frederick99 Como eu disse, a solução aqui corresponde à implementação os.path.splitextem Python. Se essa implementação é sensata para possíveis contribuições controversas é outro tópico.
22318 Cyker
Como funcionam as aspas dentro do padrão ( "$root")? O que poderia acontecer se eles fossem omitidos? (Não consegui encontrar nenhuma documentação sobre o assunto.) Além disso, como isso lida com nomes de arquivos com *ou ?neles?
ymett
Ok, o teste me mostra que as aspas tornam o padrão literal, ou seja, *e ?não são especiais. Então as duas partes da minha pergunta se respondem. Estou certo de que isso não está documentado? Ou isso deve ser entendido pelo fato de que as aspas desativam a expansão glob em geral?
ymett
Resposta brilhante! Vou apenas sugerir uma variante um pouco mais simples para calcular a raiz: root="${path#?}";root="${path::1}${root%.*}"- proceda da mesma forma para extrair a extensão.
Maëlan
26

Você pode usar o cutcomando para remover as duas últimas extensões (a ".tar.gz"parte):

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

Como observado por Clayton Hughes em um comentário, isso não funcionará no exemplo real da pergunta. Portanto, como alternativa, proponho o uso de sedexpressões regulares estendidas, assim:

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

Ele funciona removendo as duas últimas extensões (alfanuméricas) incondicionalmente.

[Atualizado novamente após comentário de Anders Lindahl]

Algum cara programador
fonte
4
Isso funciona apenas no caso em que o nome do arquivo / caminho não contém outros pontos: echo "mpc-1.0.1.tar.gz" | cut -d '.' --complement -f2- produz "MPC-1" (apenas os primeiros 2 domínios após delimitando por.)
Clayton Hughes
@ClaytonHughes Você está correto, e eu deveria ter testado melhor. Adicionada outra solução.
Algum programador
As expressões sed devem ser usadas $para verificar se a extensão correspondente está no final do nome do arquivo. Caso contrário, um nome de arquivo como esse i.like.tar.gz.files.tar.bz2pode produzir um resultado inesperado.
precisa
@AndersLindahl Ainda será, se a ordem das extensões for o inverso da sedordem da cadeia. Mesmo com $no final um nome de arquivo como mpc-1.0.1.tar.bz2.tar.gzo remove ambos .tar.gze depois .tar.bz2.
Algum programador
$ echo "foo.tar.gz" | cut -d '.' -f2- SEM --complement fará com que o segundo item de divisão chegue ao final da string $ echo "foo.tar.gz" | cut -d '.' -f2- tar.gz
Gene Black
23

Aqui estão algumas sugestões alternativas (principalmente em awk), incluindo alguns casos de uso avançados, como extrair números de versão para pacotes 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 os casos de uso estão usando o caminho completo original como entrada, sem depender dos resultados intermediários.

henfiber
fonte
20

A resposta aceita funciona bem no típico casos , mas não em ponta casos , a saber:

  • Para nomes de arquivos sem extensão (chamado sufixo no restante desta resposta), extension=${filename##*.}retorna o nome do arquivo de entrada em vez de uma sequência vazia.
  • extension=${filename##*.}não inclui o inicial ., contrário à convenção.
    • Anexar cegamente . não funcionaria para nomes de arquivos sem sufixo.
  • filename="${filename%.*}" será a sequência vazia, se o nome do arquivo de entrada começar com . e não contiver mais .caracteres (por exemplo, .bash_profile) - ao contrário da convenção.

---------

Assim, a complexidade de uma solução robusta que cobre todos os casos extremos exige uma função - veja sua definição abaixo; ele pode retornar todos os componentes de um caminho .

Chamada de exemplo:

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

Observe que os argumentos após o caminho de entrada são escolhidos livremente, nomes de variáveis ​​posicionais .
Para ignorar variáveis ​​que não são de interesse anteriores a essas, especifique _(para usar a variável de descarte $_) ou ''; por exemplo, para extrair apenas a raiz e a extensão do nome do arquivo, 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 teste que exerce a função:

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

Saída esperada - observe os casos extremos:

  • um nome de arquivo sem sufixo
  • um nome de arquivo começando com .( não considerado o início do sufixo)
  • um caminho de entrada que termina em / (à direita /é ignorado)
  • um caminho de entrada que é apenas um nome de arquivo (. é retornado como o caminho pai)
  • um nome de arquivo com um .token com mais de prefixo (apenas o último é considerado o sufixo):
----- /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
fonte
19

A solução mais pequena e mais simples (em linha única) é:

$ file=/blaabla/bla/blah/foo.txt
echo $(basename ${file%.*}) # foo
Ron
fonte
Esse é um uso inútilecho . Em geral, echo $(command)é melhor escrever simplesmente a commandmenos que você exija especificamente que o shell execute a tokenização de espaço em branco e a expansão de curinga na saída commandantes de exibir o resultado. Quiz: qual é o resultado echo $(echo '*')(e se é isso que você realmente quer, você realmente quer apenas echo *).
tripleee
@ Triplee Eu não usei nenhum echocomando. Eu apenas o usei para demonstrar o resultado fooque está aparecendo na 3ª linha como resultado da 2ª linha.
Ron
Mas apenas basename "${file%.*}"faria o mesmo; você está usando uma substituição de comando para capturar sua saída, apenas para echoa mesma saída imediatamente. (Sem citar, o resultado é nominalmente diferente; mas isso é pouco relevante, muito menos uma característica, aqui.)
tripleee
basename "$file" .txtEvita também a complexidade da substituição de parâmetros.
Tripleee
1
@ Ron Leia seu primeiro comentário antes de acusá-lo de desperdiçar nosso tempo.
Frederick99
14

Eu acho que se você só precisa do nome do arquivo, pode tentar o seguinte:

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 isso é tudo = D.

Andrew Woolfgang
fonte
Só queria BASEDIRECTORY :) Obrigado!
Carlos Ricardo
12

Você pode forçar o corte para exibir todos os campos e os subsequentes adicionando -ao número do campo.

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

Portanto, se FILE estiver eth0.pcap.gz, a EXTENSÃO serápcap.gz

Usando a mesma lógica, você também pode buscar o nome do arquivo usando '-' com o seguinte:

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

Isso funciona mesmo para nomes de arquivos que não têm nenhuma extensão.

maciek gajewski
fonte
8

Reconhecimento de arquivo mágico

Além das muitas boas respostas sobre essa questão do Stack Overflow, gostaria de acrescentar:

No Linux e outro unixen, há um comando mágico chamado file, que detecta o tipo de arquivo analisando alguns primeiros bytes de arquivo. Esta é uma ferramenta muito antiga, usada inicialmente para servidores de impressão (se não for criada para ... não tenho certeza disso).

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

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

Extensões de padrões podem ser encontradas em /etc/mime.types(na minha área de trabalho Debian GNU / Linux. Veja man filee man mime.types. Talvez você precise instalar o fileutilitário e os mime-supportpacotes):

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

Você pode criar um função para determinar a extensão correta. Há uma pequena amostra (não perfeita):

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 função pode definir uma variável Bash que pode ser usada posteriormente:

(Isso é inspirado na resposta correta do @Petesh):

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

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

Ok, então se eu entendi corretamente, o problema aqui é como obter o nome e a extensão completa de um arquivo que possui várias extensões, por exemplo stuff.tar.gz,.

Isso funciona para mim:

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

Isso fornecerá o stuffnome do arquivo e a .tar.gzextensão. Funciona para qualquer número de extensões, incluindo 0. Espero que ajude a qualquer pessoa que tenha o mesmo problema =)

Al3xXx
fonte
O resultado correto (de acordo com os.path.splitexto que o OP deseja) é ('stuff.tar', '.gz').
Cyker 2/12/16
6

Eu uso o seguinte script

$ echo "foo.tar.gz"|rev|cut -d"." -f3-|rev
foo
Joydip Datta
fonte
Isso não é nada eficiente. Para fazer bifurcações muitas vezes, o que é bastante desnecessário, pois essa operação pode ser realizada no Bash puro, sem a necessidade de comandos e bifurcações externos.
precisa saber é o seguinte
5
$ F = "text file.test.txt"  
$ echo ${F/*./}  
txt  

Isso atende a vários pontos e espaços em um nome de arquivo; no entanto, se não houver extensão, ele retornará o próprio nome do arquivo. Fácil de verificar; basta testar se o nome do arquivo e a extensão são os mesmos.

Naturalmente, esse método não funciona para arquivos .tar.gz. No entanto, isso pode ser tratado em um processo de duas etapas. Se a extensão for gz, verifique novamente se há também uma extensão tar.

Miriam English
fonte
5

Como extrair o nome do arquivo e a extensão no peixe :

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

Advertências: Divide no último ponto, o que funciona bem para nomes de arquivos com pontos, mas não para extensões com pontos. Veja o exemplo abaixo.

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.

Provavelmente existem maneiras melhores de fazer isso. Sinta-se livre para editar minha resposta para melhorá-la.


Se você estiver lidando com um conjunto limitado de extensões e conhecer todas elas, tente o seguinte:

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

Isso não tem a ressalva como o primeiro exemplo, mas você precisa lidar com todos os casos para que seja mais tedioso, dependendo de quantas extensões você pode esperar.

Dennis
fonte
4

Aqui está o código com o AWK . Isso pode ser feito de maneira mais simples. Mas eu não sou bom no 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
fonte
Você não deve precisar da primeira instrução awk no último exemplo, certo?
BPMPitMonkey
Você pode evitar canalizar o Awk para o Awk fazendo outro split(). awk -F / '{ n=split($2, a, "."); print a[n] }' uses / `como o delimitador de nível superior, mas depois divide os segundos campos .e imprime o último elemento da nova matriz.
tripleee
4

Basta usar ${parameter%word}

No seu caso:

${FILE%.*}

Se você quiser testá-lo, siga todos os trabalhos a seguir e remova a extensão:

FILE=abc.xyz; echo ${FILE%.*};
FILE=123.abc.xyz; echo ${FILE%.*};
FILE=abc; echo ${FILE%.*};
enyo
fonte
2
Por que o voto negativo? Ainda é útil, embora não deva haver espaços ao redor das =placas.
SilverWolf - Restabelece Monica
1
Isso funciona bem. Obrigado! (agora não tem espaços em torno dos sinais de igual, se foi por isso que foi rebaixado)
Alex. S.
3

Com base na resposta Petesh , se apenas o nome do arquivo for necessário, o caminho e a extensão poderão ser removidos em uma única linha,

filename=$(basename ${fullname%.*})
cvr
fonte
Não funcionou para mim: "basename: operando ausente Tente 'basename --help' para obter mais informações."
helmy
Estranho, você tem certeza de que está usando o Bash? No meu caso, com as duas versões 3.2.25 (antigo CentOS) e 4.3.30 (Debian Jessie), ele funciona perfeitamente.
cvr
Talvez haja um espaço no nome do arquivo? Tente usarfilename="$(basename "${fullname%.*}")"
Adrian
O segundo argumento para basenameé opcional, mas especifica a extensão a ser removida. A substituição ainda pode ser útil, mas talvez basenamenão seja, uma vez que você pode realmente executar todas essas substituições com os componentes internos do shell.
tripleee
3

Baseada em grande parte nos excelentes e úteis bashismos aleatórios e úteis do @ mklement0 - além de outras respostas para essas / outras perguntas / "essa maldita Internet" ... Eu envolvi tudo isso de uma forma um pouco mais compreensível, função reutilizável para o meu (ou o seu) .bash_profileque cuida do que (eu considero) deve ser uma versão mais robusta do dirname/ basename/ o que você tem ..

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
}     

Exemplos 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
fonte
1
Bem feito; algumas sugestões: - Você não parece confiar em $IFSnada (e, se estivesse, poderia usar localpara localizar o efeito de defini-lo). - Melhor usar localvariáveis. - Sua mensagem de erro deve ser enviada para stderr, não stdout(uso 1>&2), e você deve retornar um código de saída diferente de zero. - Melhor renomear fullnamepara basename(o primeiro sugere um caminho com componentes dir). - nameacrescenta incondicionalmente a .(ponto final), mesmo que o original não tivesse nenhum. Você pode simplesmente usar o basenameutilitário, mas observe que ele ignora uma finalização /.
mklement0
2

Uma resposta simples:

Para expandir a resposta das variáveis ​​POSIX , observe que você pode fazer padrões mais interessantes. Portanto, para o caso detalhado aqui, você pode simplesmente fazer o seguinte:

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

Isso eliminará a última ocorrência de .tar. <algo> .

De maneira mais geral, se você deseja remover a última ocorrência de. <algo> . <algo-algo> então

${1.*.*}

deve funcionar bem.

O link da resposta acima parece estar morto. Aqui está uma ótima explicação de várias manipulações de string que você pode fazer diretamente no Bash, a partir do TLDP .

RandyP
fonte
Existe uma maneira de fazer a correspondência não diferenciar maiúsculas de minúsculas?
tonix
2

Se você também deseja permitir extensões vazias , este é o mais curto possível:

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

1a linha explicada: Corresponde a PATH.EXT ou ANYTHING e substitui-o por EXT. Se QUALQUER COISA corresponder, o grupo ext não será capturado.

phil294
fonte
2

Este é o único que funcionou para mim:

path='folder/other_folder/file.js'

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

>> file

Isso também pode ser usado na interpolação de strings, mas, infelizmente, você deve definir com baseantecedência.

Ken Mueller
fonte
1

Aqui está o algoritmo que eu usei para encontrar o nome e a extensão de um arquivo quando escrevi um script Bash para tornar os nomes exclusivos quando os nomes conflitavam em relação à maiúsculas e minúsculas.

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

O teste executado.

$ 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: O programa completo de transliteração e mais casos de teste podem ser encontrados aqui: https://www.dropbox.com/s/4c6m0f2e28a1vxf/avoid-clashes-code.zip?dl=0

carimbo de história
fonte
De todas as soluções, este é o único que retorna uma string vazia quando o arquivo não tem extensão com:extension=$([[ "$theFileName" == *.* ]] && echo ".${theFileName##*.}" || echo '')
f0nzie
1

Usando o arquivo de exemplo /Users/Jonathan/Scripts/bash/MyScript.sh, este código:

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

resultará em ${ME}ser MyScripte ${MY_EXT}ser .sh:


Roteiro:

#!/bin/bash
set -e

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

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

Alguns testes:

$ ./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
fonte
2
Não sei por que isso tem tantos votos negativos - é realmente mais eficiente do que a resposta aceita. (Como o último, ele também quebra com os nomes dos arquivos de entrada sem uma extensão). Usar um caminho explícito para basenameé, talvez, um exagero.
usar o seguinte comando
1

A partir das respostas acima, o menor delineador para imitar o Python

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

presumindo que seu arquivo realmente tem uma extensão, é

EXT="${PATH##*.}"; FILE=$(basename "$PATH" .$EXT)
commonpike
fonte
Eu tenho votos negativos sobre isso. Estou pensando em remover a resposta, as pessoas de alguma forma não gostam disso.
commonpike
basename não remove a extensão, apenas o caminho.
David Cullen
Faz tanto tempo desde que olhei para a página de manual que esqueci a opção SUFFIX.
David Cullen
Você precisa saber qual extensão você deseja retirar antes de saber o que colocar, EXTpara que se tratem de tartarugas. (Além disso, você deve evitar todas as letras maiúsculas nos nomes de variáveis ​​particulares; elas são reservadas para variáveis ​​do sistema.) #
Triplee