bash - serie - shell script




Extraia o nome do arquivo e a extensão no Bash (20)

Eu quero pegar 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 . personagens. Se, digamos, eu tiver abjs , ele considerará a e b.js , em vez de ab e js .

Pode ser facilmente feito em Python com

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

mas prefiro não ativar um interpretador Python apenas para isso, se possível.

Alguma idéia melhor?


Reconhecimento de arquivos mágicos

Além das muitas boas respostas nesta questão do , gostaria de adicionar:

No Linux e em outros unixen, existe um comando magic chamado file , que faz a detecção do tipo de file , analisando alguns primeiros bytes do 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 (no meu desktop Debian GNU / Linux. Veja man file e man mime.types . Talvez você tenha que instalar o utilitário de file e mime-support pacotes de mime-support ):

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

Você poderia criar uma função bash 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 de Bash que pode ser usada mais tarde:

(Isso é inspirado na resposta correta @Petesh):

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

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

Aqui estão algumas sugestões alternativas (principalmente no awk ), incluindo alguns casos de uso avançados, como a extração de 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 de resultados intermediários.


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

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.


Eu uso o seguinte script

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

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

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

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

Saídas:

filename: thisfile
extension: txt

Não há necessidade de se preocupar com awk ou sed ou mesmo perl para esta tarefa simples. Existe uma solução compatível com o os.path.splitext() -Bash, os.path.splitext() 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 forma que o root + ext == path e ext esteja vazio ou comece com um período e contenha no máximo um período. 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 de Bash

Honrando os períodos principais

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

Ignorando os períodos principais

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

Testes

Aqui estão os casos de teste para a implementação Ignoring leading periods , que deve 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.


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

Isso funciona para mim:

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

Isto lhe dará stuff como filename e .tar.gz como extensão. Ele funciona para qualquer número de extensões, incluindo 0. Espero que isso ajude para qualquer um que tenha o mesmo problema =)


Primeiro, obtenha o nome do arquivo sem o caminho:

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

Alternativamente, você pode se concentrar no último '/' do caminho ao invés do '.' que deve funcionar mesmo se você tiver extensões de arquivo imprevisíveis:

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

Você pode querer verificar a documentação:


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

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

Então, se FILE for 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 corte:

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

Isso funciona mesmo para nomes de arquivos que não possuem nenhuma extensão.


Você pode usar a magia das variáveis ​​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 nome do seu arquivo fosse da forma ./somefile.tar.gz então echo ${FILENAME%%.*} Removeria avidamente a correspondência mais longa para o arquivo . e você teria 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

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

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

Como observou Clayton Hughes em um comentário, isso não funcionará para o exemplo real da questão. Então, como alternativa, proponho usar o sed com expressõ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]


[Revisado de um one-liner para uma função bash genérica, o comportamento agora é consistente com os utilitários dirname e basename ; lógica adicionada.]

A resposta aceita funciona bem em casos típicos , mas falha em casos extremos, a saber:

  • Para nomes de arquivos sem extensão (chamado de sufixo no restante desta resposta), extension=${filename##*.} Retorna o nome do arquivo de entrada em vez de uma string vazia.
  • extension=${filename##*.} não inclui o inicial . , ao contrário da convenção.
    • Cegamente antes . não funcionaria para nomes de arquivos sem sufixo.
  • filename="${filename%.*}" será a string vazia, se o nome do arquivo de entrada começar . e não contém 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 de borda requer uma função - veja sua definição abaixo; ele pode retornar todos os componentes de um caminho .

Exemplo de chamada:

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 pular variáveis ​​que não são de interesse que vêm antes daquelas que estão, especifique _ (para usar a variável descartável $_ ) ou '' ; Por exemplo, para extrair somente a raiz e a extensão do nome de arquivo, use a 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 de borda:

  • 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 terminando em / (à direita / é ignorado)
  • um caminho de entrada que é apenas um nome de arquivo ( . é retornado como o caminho pai)
  • um nome de arquivo com mais de -prefixed token (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

Das respostas acima, o oneliner mais curto 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)

Para tornar o diretório mais útil (no caso de um arquivo local sem caminho ser especificado como entrada), fiz o seguinte:

# Substring from 0 thru pos of filename
dir="${fullpath:0:${#fullpath} - ${#filename}}"
if [[ -z "$dir" ]]; then
    dir="./"
fi

Isso permite que você faça algo útil como adicionar um sufixo ao nome de base do arquivo de entrada como:

outfile=${dir}${base}_suffix.${ext}

testcase: foo.bar
dir: "./"
base: "foo"
ext: "bar"
outfile: "./foo_suffix.bar"

testcase: /home/me/foo.bar
dir: "/home/me/"
base: "foo"
ext: "bar"
outfile: "/home/me/foo_suffix.bar"

Uma resposta simples:

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

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

Isso cortará a última ocorrência de .tar. <alguma coisa> .

Mais geralmente, se você quisesse remover a última ocorrência de. <alguma coisa> . <else-else> então

${1.*.*}

deve funcionar bem.

O link da resposta acima parece estar morto. Aqui está uma ótima explicação de um monte de manipulação de strings que você pode fazer diretamente no Bash, do TLDP .


Você pode usar

sed 's/^/./' | rev | cut -d. -f2- | rev | cut -c2-

para obter o nome do arquivo e

sed 's/^/./' | rev | cut -d. -f1  | rev

para obter extensão.

Caso de teste:

echo "filename.gz"     | sed 's/^/./' | rev | cut -d. -f2- | rev | cut -c2-
echo "filename.gz"     | sed 's/^/./' | rev | cut -d. -f1  | rev
echo "filename"        | sed 's/^/./' | rev | cut -d. -f2- | rev | cut -c2-
echo "filename"        | sed 's/^/./' | rev | cut -d. -f1  | rev
echo "filename.tar.gz" | sed 's/^/./' | rev | cut -d. -f2- | rev | cut -c2-
echo "filename.tar.gz" | sed 's/^/./' | rev | cut -d. -f1  | rev

Com base em grande parte do excelente e cheio de aleatórios e úteis bashisms do @mklement0 - bem como outras respostas a esta / outras questões / "que maldita internet" ... eu envolvi tudo em um pouco, um pouco mais compreensível, função reutilizável para o meu (ou o seu) .bash_profileque cuida do que (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 extesionless 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>

Criando a partir da resposta Petesh , se apenas o nome do arquivo for necessário, o caminho e a extensão podem ser removidos em uma única linha,

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

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

Isso serve para vários pontos e espaços em um nome de arquivo, no entanto, se não houver nenhuma extensão, ele retorna o nome do arquivo. Fácil de verificar se; apenas teste para o nome do arquivo e extensão sendo o mesmo.

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


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 para NAME substitui um "." caractere seguido por qualquer número de não "." caracteres até o final da linha, sem nada (isto é, remove tudo, desde o final "." até o final da linha, inclusive). Esta é basicamente uma substituição não-gananciosa usando truques de regex.

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





filenames