Maneira mais curta de extrair os últimos 3 caracteres do nome do arquivo base (menos o sufixo)

12

Eu estou tentando definir uma variável em um script sh para os últimos 3 caracteres do nome base de um arquivo (por nome base, quero dizer, sem o caminho e sem o sufixo). Consegui fazer isso, mas, por pura curiosidade, estou me perguntando se existe um comando mais curto e único que eu possa usar. Originalmente eu tinha uma linha awk, mas era bastante longa. Atualmente, tenho esse script de duas linhas (assumindo que um nome de arquivo completo esteja $1):

filebase=`basename "$1"`
lastpart=`echo -n ${filebase%.*} | tail -c3`

Por exemplo, "/path/to/somefile.txt" termina com "ile" em $lastpart.

Posso de alguma forma combinar basenameo bit para separar o sufixo em um único comando e existe uma maneira de enviá-lo paratail (ou qualquer outra coisa que eu possa usar) sem usar um pipe? O sufixo é desconhecido, portanto não posso basear como parâmetro basename.

O objetivo principal não é tanto o mais curto possível, mas o mais legível possível. O contexto real de tudo isso é essa pergunta no Superusuário , onde estou tentando encontrar uma resposta razoavelmente simples.

Jason C
fonte
2
Como você lidaria com um arquivo como file.one.two.three? Você quer ileou two?
terdon
@terdon twofuncionaria; a extensão disso seria .threeeu acho.
Jason C

Respostas:

6

Esse é um trabalho típico para expr:

$ file=/path/to/abcdef.txt
$ expr "/$file" : '.*\([^/.]\{3\}\)\.[^/.]*$'
def

Se você souber que os nomes de arquivos têm o formato esperado (contém um e apenas um ponto e pelo menos três caracteres antes do ponto), isso pode ser simplificado para:

expr "/$file" : '.*\(.\{3\}\)\.'

Observe que o status de saída será diferente de zero se não houver correspondência, mas também se a parte correspondente for um número que resolva para 0. (como para a000.txtou a-00.txt)

Com zsh:

file=/path/to/abcdef.txt
lastpart=${${file:t:r}[-3,-1]}

( :tpara cauda (nome da base), :rpara descanso (com extensão removida)).

Stéphane Chazelas
fonte
2
Agradável. expré outro com o qual eu preciso me familiarizar. Eu realmente gosto das zshsoluções em geral (eu estava lendo sobre seu suporte para substituições aninhadas no lado esquerdo de um ${}ontem e desejando shter o mesmo), é apenas uma chatice que nem sempre esteja presente por padrão.
Jason C
2
@ JasonC - a informação mais importa. Torne o melhor possível o mais acessível possível - esse é o objetivo do sistema. Se rep comprou comida eu poderia ficar chateado, mas mais frequentemente (que não) informações traz para casa o bacon
mikeserv
1
@mikeserv "Pedido: representante de troca de bacon"; cuidado meta aqui vou eu.
Jason C
1
O @mikerserv, o seu é o POSIX, usa apenas os recursos internos e não bifurca nenhum processo. Não usar a substituição de comandos também significa evitar problemas com novas linhas à direita, por isso é uma boa resposta também.
Stéphane Chazelas
1
@ MikeServ, eu não quis dizer que nãoexpr era POSIX. Certamente é. Raramente está embutido.
Stéphane Chazelas
13
var=123456
echo "${var#"${var%???}"}"

###OUTPUT###

456

Essa primeira remove os últimos três caracteres e $vardepois remove $varos resultados dessa remoção - que retorna os últimos três caracteres de $var. Aqui estão alguns exemplos mais especificamente destinados a demonstrar como você pode fazer isso:

touch file.txt
path=${PWD}/file.txt
echo "$path"

/tmp/file.txt

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

file
txt
file.txt
/tmp/file.txt

Você não precisa espalhar tudo isso através de tantos comandos. Você pode compactar isso:

{
    base=${path##*/} exten= 
    printf %s\\n "${base%.*}" "${exten:=${base#"${base%???}"}}" "$base" "$path"
    echo "$exten"
}

file 
txt 
file.txt 
/tmp/file.txt
txt

Combinar $IFScom setos parâmetros ting shell também pode ser um meio muito eficaz de analisar e perfurar as variáveis ​​do shell:

(IFS=. ; set -f; set -- ${path##*/}; printf %s "${1#"${1%???}"}")

Isso vai ficar só três personagens imediatamente anterior ao primeiro período após o último /em $path. Se você deseja recuperar apenas os três primeiros caracteres imediatamente anteriores ao último .em $path (por exemplo, se houver a possibilidade de mais de um .no nome do arquivo) :

(IFS=.; set -f; set -- ${path##*/}; ${3+shift $(($#-2))}; printf %s "${1#"${1%???}"}")

Nos dois casos, você pode fazer:

newvar=$(IFS...)

E...

(IFS...;printf %s "$2")

... imprimirá o que segue .

Se você não se importa de usar um programa externo, pode:

printf %s "${path##*/}" | sed 's/.*\(...\)\..*/\1/'

Se houver a chance de um \ncaractere ewline no nome do arquivo (não aplicável às soluções de shell nativas - todos eles lidam com isso de qualquer maneira) :

printf %s "${path##*/}" | sed 'H;$!d;g;s/.*\(...\)\..*/\1/'
mikeserv
fonte
1
É obrigado. Eu também encontrei documentação . Mas para obter os três últimos personagens de $baselá, o melhor que pude fazer foi a de três linhas name=${var##*/} ; base=${name%%.*} ; lastpart=${base#${base%???}}. No lado positivo, é pura festa, mas ainda são 3 linhas. (No seu exemplo de "/tmp/file.txt", eu precisaria de "ile" em vez de "file".) Acabei de aprender muito sobre a substituição de parâmetros; Eu não tinha ideia de que poderia fazer isso ... muito útil. Eu também acho muito legível, pessoalmente.
Jason C
1
@ JasonC - este é um comportamento totalmente portátil - não é específico do bash. Eu recomendo ler isso .
mikeserv
1
Bem, eu acho que posso usar em %vez de %%remover o sufixo, e na verdade não preciso desviar o caminho, para obter uma linha melhor, duas noextn=${var%.*} ; lastpart=${noextn#${noextn%???}}.
Jason C
1
@ JasonC - sim, parece que iria funcionar. Ele vai quebrar se houver $IFSem ${noextn}e você não citar a expansão. Então, isso é mais seguro:lastpart=${noextn#"${noextn%???}"}
mikeserv 23/06
1
@ JasonC - por último, se você achou o item útil acima, você pode querer olhar para isso . Ele lida com outras formas de expansão de parâmetros e as outras respostas a essa pergunta também são realmente boas. E há links para duas outras respostas sobre o mesmo assunto. Se você quiser.
mikeserv
4

Se você pode usar perl:

lastpart=$(
    perl -e 'print substr((split(/\.[^.]*$/,shift))[0], -3, 3)
            ' -- "$(basename -- "$1")"
)
cuonglm
fonte
isso é legal. tem ny voto.
mikeserv
Um pouco mais conciso: perl -e 'shift =~ /(.{3})\.[^.]*$/ && print $1' $filename. Um adicional basenameseria necessário se o nome do arquivo não contenha sufixo, mas algum diretório no caminho contenha.
Dubu
@ Dubu: Sua solução sempre falha se o nome do arquivo não tiver sufixo.
cuonglm
1
@Gnouc Isso foi por intenção. Mas você está certo, isso pode estar errado, dependendo da finalidade. Alternativa:perl -e 'shift =~ m#(.{3})(?:\.[^./]*)?$# && print $1' $filename
Dubu
2

sed trabalha para isso:

[user@host ~]$ echo one.two.txt | sed -r 's|(.*)\..*$|\1|;s|.*(...)$|\1|'
two

Ou

[user@host ~]$ sed -r 's|(.*)\..*$|\1|;s|.*(...)$|\1|' <<<one.two.txt
two

Se o seu sednão suportar -r, substitua as instâncias de ()com \(e \), e então -rnão é necessário.

BenjiWiebe
fonte
1

Se o perl estiver disponível, acho que ele pode ser mais legível do que outras soluções, especificamente porque sua linguagem regex é mais expressiva e possui o /xmodificador, que permite escrever regexs mais claros:

perl -e 'print $1 if shift =~ m{ ( [^/]{3} ) [.] [^./]* \z }x' -- "$file"

Isso não imprime nada se não houver essa correspondência (se o nome da base não tiver extensão ou se a raiz antes da extensão for muito curta). Dependendo dos seus requisitos, você pode ajustar a regex. Este regex reforça as restrições:

  1. Corresponde aos 3 caracteres antes da extensão final (a parte após e incluindo o último ponto). Esses três caracteres podem conter um ponto.
  2. A extensão pode estar vazia (exceto o ponto).
  3. A parte correspondente e a extensão devem fazer parte do nome da base (a parte após a última barra).

Usar isso em uma substituição de comando tem os problemas normais com a remoção de muitas novas linhas finais, um problema que também afeta a resposta de Stéphane. Pode ser tratado nos dois casos, mas é um pouco mais fácil aqui:

lastpart=$(
  perl -e 'print "$1x" if shift =~ m{ ( [^/]{3} ) [.] [^./]* \z }x' -- "$file"
)
lastpart=${lastpart%x}  # allow for possible trailing newline
jrw32982 suporta Monica
fonte
0

Python2.7

$ echo /path/to/somefile.txt | python -c "import sys, os; print '.'.join(os.path.basename(sys.stdin.read()).split('.')[:-1])[-3:]"
ile

$ echo file.one.two.three | python -c "import sys, os; print '.'.join(os.path.basename(sys.stdin.read()).split('.')[:-1])[-3:]"
two
HVNSweeting
fonte
0

Eu acho que essa função bash, pathStr (), fará o que você está procurando.

Não requer awk, sed, grep, perl ou expr. Ele usa apenas bash builtins, por isso é bastante rápido.

Também incluí as funções dependentes argsNumber e isOption, mas suas funcionalidades podem ser facilmente incorporadas ao pathStr.

A função dependente ifHelpShow não está incluída, pois possui inúmeras subdependências para a saída do texto de ajuda na linha de comando do terminal ou em uma caixa de diálogo da GUI via YAD . O texto de ajuda passado para ele é incluído na documentação. Informe se você gostaria do ifHelpShow e de seus dependentes.

function  pathStr () {
  ifHelpShow "$1" 'pathStr --OPTION FILENAME
    Given FILENAME, pathStr echos the segment chosen by --OPTION of the
    "absolute-logical" pathname. Only one segment can be retrieved at a time and
    only the FILENAME string is parsed. The filesystem is never accessed, except
    to get the current directory in order to build an absolute path from a relative
    path. Thus, this function may be used on a FILENAME that does not yet exist.
    Path characteristics:
        File paths are "absolute" or "relative", and "logical" or "physical".
        If current directory is "/root", then for "bashtool" in the "sbin" subdirectory ...
            Absolute path:  /root/sbin/bashtool
            Relative path:  sbin/bashtool
        If "/root/sbin" is a symlink to "/initrd/mnt/dev_save/share/sbin", then ...
            Logical  path:  /root/sbin/bashtool
            Physical path:  /initrd/mnt/dev_save/share/sbin/bashtool
                (aka: the "canonical" path)
    Options:
        --path  Absolute-logical path including filename with extension(s)
                  ~/sbin/file.name.ext:     /root/sbin/file.name.ext
        --dir   Absolute-logical path of directory containing FILENAME (which can be a directory).
                  ~/sbin/file.name.ext:     /root/sbin
        --file  Filename only, including extension(s).
                  ~/sbin/file.name.ext:     file.name.ext
        --base  Filename only, up to last dot(.).
                  ~/sbin/file.name.ext:     file.name
        --ext   Filename after last dot(.).
                  ~/sbin/file.name.ext:     ext
    Todo:
        Optimize by using a regex to match --options so getting argument only done once.
    Revised:
        20131231  docsalvage'  && return
  #
  local _option="$1"
  local _optarg="$2"
  local _cwd="$(pwd)"
  local _fullpath=
  local _tmp1=
  local _tmp2=
  #
  # validate there are 2 args and first is an --option
  [[ $(argsNumber "$@") != 2 ]]                        && return 1
  ! isOption "$@"                                      && return 1
  #
  # determine full path of _optarg given
  if [[ ${_optarg:0:1} == "/" ]]
  then
    _fullpath="$_optarg"
  else
    _fullpath="$_cwd/$_optarg"
  fi
  #
  case "$_option" in
   --path)  echo "$_fullpath"                            ; return 0;;
    --dir)  echo "${_fullpath%/*}"                       ; return 0;;
   --file)  echo "${_fullpath##*/}"                      ; return 0;;
   --base)  _tmp1="${_fullpath##*/}"; echo "${_tmp1%.*}" ; return 0;;
    --ext)  _tmp1="${_fullpath##*/}";
            _tmp2="${_tmp1##*.}";
            [[ "$_tmp2" != "$_tmp1" ]]  && { echo "$_tmp2"; }
            return 0;;
  esac
  return 1
}

function argsNumber () {
  ifHelpShow "$1" 'argsNumber "$@"
  Echos number of arguments.
  Wrapper for "$#" or "${#@}" which are equivalent.
  Verified by testing on bash 4.1.0(1):
      20140627 docsalvage
  Replaces:
      argsCount
  Revised:
      20140627 docsalvage'  && return
  #
  echo "$#"
  return 0
}

function isOption () {
  # isOption "$@"
  # Return true (0) if argument has 1 or more leading hyphens.
  # Example:
  #     isOption "$@"  && ...
  # Note:
  #   Cannot use ifHelpShow() here since cannot distinguish 'isOption --help'
  #   from 'isOption "$@"' where first argument in "$@" is '--help'
  # Revised:
  #     20140117 docsalvage
  # 
  # support both short and long options
  [[ "${1:0:1}" == "-" ]]  && return 0
  return 1
}

RECURSOS

DocSalvager
fonte
Eu não entendo - já foi demonstrado aqui como fazer coisas totalmente portáteis - sem bashismos - aparentemente mais simples do que isso. Além disso, o que é ${#@}?
mikeserv
Isso apenas empacota a funcionalidade em uma função reutilizável. re: $ {# @} ... A manipulação de matrizes e seus elementos requer a notação completa da variável $ {}. $ @ é a 'matriz' de argumentos. $ {# @} é a sintaxe do bash para o número de argumentos.
DocSalvager
Não, $#é a sintaxe do número de argumentos e também é usada em outros lugares aqui.
mikeserv
Você está certo de que "$ #" é o systax amplamente documentado para "número de argumentos". No entanto, acabei de reverificar que "$ {# @}" é equivalente. Acabei com isso depois de experimentar as diferenças e semelhanças entre argumentos posicionais e matrizes. O posterior vem da sintaxe da matriz, que aparentemente é sinônimo da sintaxe "$ #" mais curta e simples. Alterei e documentei argsNumber () para usar "$ #". Obrigado!
DocSalvager
${#@}não é equivalente na maioria dos casos - a especificação POSIX indica os resultados de qualquer expansão de parâmetro em um $@ou $*não é especificado, infelizmente. Pode trabalhar em bashmas isso não é um recurso confiável, eu acho que é o que estou tentando dizer,.
mikeserv