Moyen le plus court d'extraire les 3 derniers caractères du nom de fichier de base (moins le suffixe)

12

J'essaie de définir une variable dans un script sh sur les 3 derniers caractères du nom de base d'un fichier (par nom de base, je veux dire sans le chemin et sans le suffixe). J'ai réussi à le faire mais, par pure curiosité, je me demande s'il y a une commande unique plus courte que je peux utiliser. À l'origine, j'avais un monoplace avec awk, mais c'était plutôt long. Actuellement, j'ai ce script à deux lignes (en supposant qu'un nom de fichier complet se trouve $1):

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

Ainsi, par exemple, "/path/to/somefile.txt" se termine par "ile" dans $lastpart.

Puis-je en quelque sorte combiner basenameet le bit pour supprimer le suffixe en une seule commande, et existe-t-il un moyen de l'envoyer à tail(ou autre chose que je peux utiliser) sans utiliser de canal? Le suffixe est inconnu, je ne peux donc pas le baser en tant que paramètre sur basename.

L'objectif principal n'est pas vraiment d'être aussi court que possible, mais d'être aussi lisible d'un coup d'œil que possible. Le contexte réel de tout cela est cette question sur Superuser , où j'essaie de trouver une réponse assez simple.

Jason C
la source
2
Comment traiteriez-vous un fichier comme file.one.two.three? Souhaitez-vous ileou two?
terdon
@terdon twofonctionnerait; l'extension sur ce serait .threeje suppose.
Jason C

Réponses:

6

C'est un travail typique pour expr:

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

Si vous savez que les noms de vos fichiers ont le format attendu (contient un et un seul point et au moins 3 caractères avant le point), cela peut être simplifié pour:

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

Notez que l'état de sortie sera différent de zéro s'il n'y a pas de correspondance, mais également si la partie correspondante est un nombre qui se résout à 0. (comme pour a000.txtou a-00.txt)

Avec zsh:

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

( :tpour queue (nom de base), :rpour reste (avec extension supprimée)).

Stéphane Chazelas
la source
2
Agréable. exprest un autre dont je dois me familiariser. J'aime vraiment les zshsolutions en général (je lisais à propos de son support pour les substitutions imbriquées sur le côté gauche d'un ${}hier aussi et souhaitant shavoir la même chose), c'est juste une déception qu'il n'est pas toujours présent par défaut.
Jason C
2
@JasonC - l'information est la plus importante. Tirez le meilleur parti de celui-ci aussi accessible que possible - c'est tout le point du système de toute façon. Si le représentant a acheté de la nourriture, je pourrais être contrarié, mais le plus souvent (plus que jamais) l' info ramène le bacon à la maison
mikeserv
1
@mikeserv "Request: Exchange rep for bacon"; regardez méta ici je viens.
Jason C
1
@mikerserv, le vôtre est POSIX, utilise uniquement des commandes internes et ne crée aucun processus. Ne pas utiliser la substitution de commandes signifie également que vous évitez les problèmes de retour à la ligne, c'est donc une bonne réponse également.
Stéphane Chazelas
1
@mikeserv, je ne voulais pas laisser entendre que ce exprn'était pas POSIX. C'est certainement. Il est cependant rarement intégré.
Stéphane Chazelas
13
var=123456
echo "${var#"${var%???}"}"

###OUTPUT###

456

Cela supprime d'abord les trois derniers caractères de $varpuis supprime $varles résultats de cette suppression - qui renvoie les trois derniers caractères de $var. Voici quelques exemples plus spécifiquement destinés à démontrer comment vous pourriez faire une telle chose:

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

Vous n'avez pas à répartir tout cela à travers autant de commandes. Vous pouvez compacter ceci:

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

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

La combinaison $IFSavec setles paramètres de shell ting peut également être un moyen très efficace d'analyser et de parcourir les variables de shell:

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

Cela ne vous donnera que les trois caractères précédant immédiatement la première période suivant la dernière /entrée $path. Si vous souhaitez récupérer uniquement les trois premiers caractères précédant immédiatement le dernier .dans $path (par exemple, s'il existe une possibilité de plus d'un .nom de fichier) :

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

Dans les deux cas, vous pouvez faire:

newvar=$(IFS...)

Et...

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

... imprimera ce qui suit le .

Si cela ne vous dérange pas d'utiliser un programme externe, vous pouvez faire:

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

S'il y a une chance qu'un \ncaractère ewline dans le nom de fichier (ne s'applique pas aux solutions de shell natives - ils gèrent tous cela de toute façon) :

printf %s "${path##*/}" | sed 'H;$!d;g;s/.*\(...\)\..*/\1/'
mikeserv
la source
1
Oui, merci. J'ai également trouvé de la documentation . Mais pour obtenir les 3 derniers caractères à partir de $baselà, le mieux que j'ai pu faire était le trio name=${var##*/} ; base=${name%%.*} ; lastpart=${base#${base%???}}. Sur le plan positif, c'est pur bash, mais c'est toujours 3 lignes. (Dans votre exemple de "/tmp/file.txt", j'aurais besoin de "ile" plutôt que de "file".) Je viens d'apprendre beaucoup de choses sur la substitution de paramètres; Je ne savais pas que ça pouvait faire ça ... assez pratique. Je le trouve également très lisible, personnellement.
Jason C
1
@JasonC - c'est un comportement entièrement portable - ce n'est pas spécifique à bash. Je recommande de lire ceci .
mikeserv
1
Eh bien, je suppose que je peux utiliser %au lieu de %%supprimer le suffixe, et je n'ai pas vraiment besoin de supprimer le chemin, donc je peux obtenir une ligne deux plus agréable noextn=${var%.*} ; lastpart=${noextn#${noextn%???}}.
Jason C
1
@JasonC - oui, cela semble fonctionner. Il se cassera s'il y $IFSen a ${noextn}et vous ne citez pas l'extension. Donc, c'est plus sûr:lastpart=${noextn#"${noextn%???}"}
mikeserv
1
@JasonC - enfin, si vous avez trouvé ce qui précède utile, vous voudrez peut-être regarder cela . Il traite d'autres formes d'expansion des paramètres et les autres réponses à cette question sont également très bonnes. Et il y a des liens vers deux autres réponses sur le même sujet. Si tu veux.
mikeserv
4

Si vous pouvez utiliser perl:

lastpart=$(
    perl -e 'print substr((split(/\.[^.]*$/,shift))[0], -3, 3)
            ' -- "$(basename -- "$1")"
)
cuonglm
la source
c'est cool. obtenu ny vote.
mikeserv
Un peu plus concis: perl -e 'shift =~ /(.{3})\.[^.]*$/ && print $1' $filename. Un supplément basenameserait nécessaire si le nom de fichier ne peut contenir aucun suffixe mais que certains répertoires du chemin en contiennent.
Dubu
@Dubu: Votre solution échoue toujours si le nom de fichier n'a pas de suffixe.
cuonglm
1
@Gnouc C'était par intention. Mais vous avez raison, cela pourrait être faux selon le but. Alternative:perl -e 'shift =~ m#(.{3})(?:\.[^./]*)?$# && print $1' $filename
Dubu
2

sed travaille pour cela:

[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

Si votre sedne prend pas en charge -r, remplacez simplement les instances de ()avec \(et \), et alors ce -rn'est pas nécessaire.

BenjiWiebe
la source
1

Si perl est disponible, je trouve qu'il peut être plus lisible que d'autres solutions, notamment parce que son langage d'expression /xrégulière est plus expressif et qu'il a le modificateur, qui permet d'écrire des expressions régulières plus claires:

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

Cela n'imprime rien s'il n'y a pas une telle correspondance (si le nom de base n'a pas d'extension ou si la racine avant l'extension est trop courte). Selon vos besoins, vous pouvez ajuster l'expression régulière. Cette expression régulière applique les contraintes:

  1. Il correspond aux 3 caractères avant l'extension finale (la partie après et y compris le dernier point). Ces 3 caractères peuvent contenir un point.
  2. L'extension peut être vide (sauf pour le point).
  3. La partie correspondante et l'extension doivent faire partie du nom de base (la partie après la dernière barre oblique).

L'utilisation de ceci dans une substitution de commande a le problème normal de supprimer trop de nouvelles lignes de fin, un problème qui affecte également la réponse de Stéphane. Il peut être traité dans les deux cas, mais c'est un peu plus facile ici:

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

Je pense que cette fonction bash, pathStr (), fera ce que vous recherchez.

Il ne nécessite pas awk, sed, grep, perl ou expr. Il n'utilise que des commandes bash, donc c'est assez rapide.

J'ai également inclus les fonctions argsNumber et isOption dépendantes mais leurs fonctionnalités pourraient être facilement incorporées dans pathStr.

La fonction dépendante ifHelpShow n'est pas incluse car elle a de nombreuses sous-dépendances pour la sortie du texte d'aide soit sur la ligne de commande du terminal, soit dans une boîte de dialogue GUI via YAD . Le texte d'aide qui lui est transmis est inclus pour la documentation. Indiquez si vous souhaitez ifHelpShow et ses dépendants.

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
}

RESSOURCES

DocSalvager
la source
Je ne comprends pas - il a déjà été démontré ici comment faire de la même manière entièrement portable - sans bashismes - apparemment plus simple que cela. Et qu'est-ce que c'est ${#@}?
mikeserv
Cela regroupe simplement la fonctionnalité dans une fonction réutilisable. re: $ {# @} ... La manipulation des tableaux et de leurs éléments nécessite la notation de variable complète $ {}. $ @ est le «tableau» d'arguments. $ {# @} est la syntaxe bash du nombre d'arguments.
DocSalvager
Non, $#c'est la syntaxe du nombre d'arguments, et elle est également utilisée ailleurs ici.
mikeserv
Vous avez raison: "$ #" est la systax largement documentée pour le "nombre d'arguments". Cependant, je viens de vérifier que "$ {# @}" est équivalent. Je me suis retrouvé avec cela après avoir expérimenté les différences et les similitudes entre les arguments positionnels et les tableaux. La dernière vient de la syntaxe du tableau qui est apparemment synonyme de la syntaxe "$ #" plus courte et plus simple. J'ai modifié et documenté argsNumber () pour utiliser "$ #". Merci!
DocSalvager
${#@}n'est pas équivalent dans la plupart des cas - la spécification POSIX indique les résultats de toute extension de paramètre sur l'un $@ou l' autre ou $*n'est pas spécifié, malheureusement. Cela peut fonctionner, bashmais ce n'est pas une fonctionnalité fiable, je suppose que c'est ce que j'essaie de dire.,
mikeserv