La forma más corta de extraer los últimos 3 caracteres del nombre de archivo base (menos sufijo)

12

Estoy tratando de establecer una variable en un script sh para los últimos 3 caracteres del nombre base de un archivo (por nombre base quiero decir sin la ruta y sin el sufijo). He logrado hacer esto pero, por pura curiosidad, me pregunto si hay un comando único más corto que pueda usar. Originalmente tenía una sola línea awk, pero fue bastante larga. Actualmente tengo este script de dos líneas (suponiendo que haya un nombre de archivo completo $1):

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

Entonces, por ejemplo, "/path/to/somefile.txt" termina con "ile" en $lastpart.

¿Puedo combinar de alguna manera basenamey el bit para pelar el sufijo en un solo comando, y hay alguna forma de enviarlo tail(o algo más que pueda usar) sin usar una tubería? El sufijo es desconocido, por lo que no puedo basarlo como un parámetro basename.

El objetivo principal no es en realidad ser lo más corto posible, sino ser lo más legible posible de un vistazo. El contexto real de todo esto es esta pregunta en Superusuario , donde estoy tratando de encontrar una respuesta razonablemente simple.

Jason C
fuente
2
¿Cómo tratarías un archivo como file.one.two.three? ¿Quieres ileo two?
terdon
@terdon twofuncionaría; la extensión en eso sería .three, supongo.
Jason C

Respuestas:

6

Ese es un trabajo típico para expr:

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

Si sabe que sus nombres de archivo tienen el formato esperado (contiene uno y solo un punto y al menos 3 caracteres antes del punto), eso puede simplificarse para:

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

Tenga en cuenta que el estado de salida será distinto de cero si no hay coincidencia, pero también si la parte coincidente es un número que se resuelve en 0. (como para a000.txto a-00.txt)

Con zsh:

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

( :tpara cola (nombre base), :rpara descanso (con extensión eliminada)).

Stéphane Chazelas
fuente
2
Agradable. expres otro con el que necesito familiarizarme. Realmente me gustan las zshsoluciones en general (estaba leyendo sobre su soporte para sustituciones anidadas en el lado izquierdo de un ${}ayer también y deseando shtener lo mismo), es un fastidio que no siempre esté presente por defecto.
Jason C
2
@JasonC: la información es lo más importante. Aproveche al máximo lo más accesible que pueda; de todos modos, ese es el punto central del sistema. Si el representante comprara comida, podría enfadarme, pero con mayor frecuencia (que nunca) la información trae a casa el tocino
mikeserv
1
@mikeserv "Solicitud: Representante de intercambio de tocino"; mira meta aquí vengo.
Jason C
1
@mikerserv, el tuyo es POSIX, usa solo builtins y no bifurca ningún proceso. No utilizar la sustitución de comandos también significa que evitará problemas con las nuevas líneas finales, por lo que también es una buena respuesta.
Stéphane Chazelas
1
@mikeserv, no quise decir que noexpr era POSIX. Ciertamente lo es. Sin embargo, rara vez está incorporado.
Stéphane Chazelas
13
var=123456
echo "${var#"${var%???}"}"

###OUTPUT###

456

Eso primero elimina los últimos tres caracteres y $varluego elimina $varlos resultados de esa eliminación, que devuelve los últimos tres caracteres de $var. Aquí hay algunos ejemplos más específicamente dirigidos a demostrar cómo puede hacer tal cosa:

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

No tiene que difundir todo esto a través de tantos comandos. Puedes compactar esto:

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

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

La combinación $IFScon setlos parámetros de shell ting también puede ser un medio muy eficaz de analizar y perforar a través de variables de shell:

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

Eso hará que sólo los tres caracteres inmediatamente anterior al primer período siguiente a la última /en $path. Si desea recuperar solo los primeros tres caracteres inmediatamente anteriores al último .en $path (por ejemplo, si existe la posibilidad de más de uno .en el nombre del archivo) :

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

En ambos casos puedes hacer:

newvar=$(IFS...)

Y...

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

... imprimirá lo que sigue al .

Si no le importa usar un programa externo, puede hacer lo siguiente:

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

Si existe la posibilidad de un \ncarácter ewline en el nombre del archivo (no aplicable para las soluciones de shell nativas, todos manejan eso de todos modos) :

printf %s "${path##*/}" | sed 'H;$!d;g;s/.*\(...\)\..*/\1/'
mikeserv
fuente
1
Lo es, gracias. También he encontrado documentación . Pero para obtener los últimos 3 personajes a partir de $baseahí, lo mejor que pude hacer fue el de tres líneas name=${var##*/} ; base=${name%%.*} ; lastpart=${base#${base%???}}. En el lado positivo, es puro golpe, pero sigue siendo 3 líneas. (En su ejemplo de "/tmp/file.txt" necesitaría "ile" en lugar de "archivo"). Aprendí mucho sobre la sustitución de parámetros; No tenía idea de que podría hacer eso ... bastante útil. Lo encuentro muy legible, también, personalmente.
Jason C
1
@JasonC: este es un comportamiento totalmente portátil, no es específico de bash. Recomiendo leer esto .
mikeserv
1
Bueno, supongo que puedo usar en %lugar de %%eliminar el sufijo, y en realidad no necesito quitar el camino, por lo que puedo obtener una mejor línea de dos líneas noextn=${var%.*} ; lastpart=${noextn#${noextn%???}}.
Jason C
1
@JasonC: sí, parece que funcionaría. Se romperá si existe $IFSen ${noextn}y no citar la expansión. Entonces, esto es más seguro:lastpart=${noextn#"${noextn%???}"}
mikeserv
1
@JasonC: por último, si consideró útil lo anterior, es posible que desee ver esto . Se trata de otras formas de expansión de parámetros y las otras respuestas a esa pregunta también son realmente buenas. Y hay enlaces a otras dos respuestas sobre el mismo tema dentro. Si tu quieres.
mikeserv
4

Si puedes usar perl:

lastpart=$(
    perl -e 'print substr((split(/\.[^.]*$/,shift))[0], -3, 3)
            ' -- "$(basename -- "$1")"
)
Cuonglm
fuente
esta genial. Conseguí un voto.
mikeserv
Un poco más concisa: perl -e 'shift =~ /(.{3})\.[^.]*$/ && print $1' $filename. Se basenamenecesitaría un adicional si el nombre de archivo puede no contener sufijo pero algún directorio en la ruta sí.
Dubu
@Dubu: su solución siempre falla si el nombre de archivo no tiene sufijo.
Cuonglm
1
@Gnouc Esto fue por intención. Pero tienes razón, esto podría estar mal dependiendo del propósito. Alternativa:perl -e 'shift =~ m#(.{3})(?:\.[^./]*)?$# && print $1' $filename
Dubu
2

sed funciona para esto:

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

O

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

Si sedno es compatible -r, simplemente reemplace las instancias de ()con \(y \), y luego -rno es necesario.

BenjiWiebe
fuente
1

Si perl está disponible, creo que puede ser más legible que otras soluciones, específicamente porque su lenguaje regex es más expresivo y tiene el /xmodificador, que permite escribir expresiones regulares más claras:

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

Esto no imprime nada si no existe dicha coincidencia (si el nombre base no tiene extensión o si la raíz antes de la extensión es demasiado corta). Dependiendo de sus requisitos, puede ajustar la expresión regular. Esta expresión regular impone las restricciones:

  1. Coincide con los 3 caracteres antes de la extensión final (la parte posterior e incluyendo el último punto). Estos 3 caracteres pueden contener un punto.
  2. La extensión puede estar vacía (excepto el punto).
  3. La parte coincidente y la extensión deben ser parte del nombre base (la parte posterior a la última barra inclinada).

El uso de esto en una sustitución de comando tiene los problemas normales con la eliminación de demasiadas nuevas líneas finales, un problema que también afecta la respuesta de Stéphane. Se puede tratar en ambos casos, pero es un poco más fácil aquí:

lastpart=$(
  perl -e 'print "$1x" if shift =~ m{ ( [^/]{3} ) [.] [^./]* \z }x' -- "$file"
)
lastpart=${lastpart%x}  # allow for possible trailing newline
jrw32982 es compatible con Monica
fuente
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
fuente
0

Creo que esta función bash, pathStr (), hará lo que estás buscando.

No requiere awk, sed, grep, perl o expr. Utiliza solo bash builtins, por lo que es bastante rápido.

También he incluido las funciones dependientes argsNumber e isOption pero sus funcionalidades podrían incorporarse fácilmente en pathStr.

La función dependiente ifHelpShow no está incluida, ya que tiene numerosas subdependencias para generar el texto de ayuda en la línea de comandos del terminal o en un cuadro de diálogo de la GUI a través de YAD . El texto de ayuda que se le pasa se incluye para documentación. Indique si desea ifHelpShow y sus dependientes.

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
fuente
No entiendo, ya se ha demostrado aquí cómo hacer algo similar de forma totalmente portátil, sin bashismos, aparentemente más simple que esto. Además, ¿qué es ${#@}?
mikeserv
Esto simplemente empaqueta la funcionalidad en una función reutilizable. re: $ {# @} ... La manipulación de matrices y sus elementos requiere la notación de variable completa $ {}. $ @ es la 'matriz' de argumentos. $ {# @} es la sintaxis bash para la cantidad de argumentos.
DocSalvager
No, $#es la sintaxis para el número de argumentos, y también se usa en otro lugar aquí.
mikeserv
Tiene razón en que "$ #" es la sintaxis ampliamente documentada para "número de argumentos". Sin embargo, acabo de verificar que "$ {# @}" es equivalente. Terminé con eso después de experimentar con las diferencias y similitudes entre argumentos posicionales y matrices. El último proviene de la sintaxis de matriz, que aparentemente es sinónimo de la sintaxis más corta y simple "$ #". He alterado y documentado argsNumber () para usar "$ #". ¡Gracias!
DocSalvager
${#@}en la mayoría de los casos, no es equivalente: la especificación POSIX indica los resultados de cualquier expansión de parámetros en cualquiera de los dos, $@o $*desafortunadamente. Puede funcionar, bashpero esa no es una característica confiable, supongo que es lo que estoy tratando de decir.,
mikeserv