Il modo più breve per estrarre gli ultimi 3 caratteri del nome file base (meno il suffisso)


12

Sto cercando di impostare una variabile in uno script sh sugli ultimi 3 caratteri del nome di base di un file (per nome di base intendo senza il percorso e senza il suffisso). Sono riuscito a farlo ma, per pura curiosità, mi chiedo se esiste un singolo comando più breve che posso usare. Inizialmente avevo un one-liner con awk, ma era piuttosto lungo. Attualmente ho questo script a due righe (supponendo che sia presente un nome file completo $1):

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

Ad esempio, "/path/to/somefile.txt" finisce con "ile" in $lastpart.

Posso in qualche modo combinare basenamee il bit per rimuovere il suffisso in un singolo comando, e c'è un modo per inviarlo tail(o qualcos'altro che posso usare) senza usare una pipe? Il suffisso è sconosciuto, quindi non posso basarlo come parametro su basename.

L'obiettivo principale non è in realtà tanto essere il più breve possibile, quanto più leggibile a prima vista. Il contesto reale di tutto ciò è questa domanda su Superuser , in cui sto cercando di trovare una risposta ragionevolmente semplice.


2
Come gestiresti un file simile file.one.two.three? Vorresti ileo two?
terdon

@terdon twoavrebbe funzionato; l'estensione su quello sarebbe .threeimmagino.
Jason C,

Risposte:


6

Questo è un lavoro tipico per expr:

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

Se sai che i nomi dei tuoi file hanno il formato previsto (contiene un solo punto e almeno 3 caratteri prima del punto), che può essere semplificato per:

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

Si noti che lo stato di uscita sarà diverso da zero se non c'è corrispondenza, ma anche se la parte corrispondente è un numero che si risolve in 0. (come per a000.txto a-00.txt)

Con zsh:

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

( :tper tail (basename), :rper il resto (con estensione rimossa)).


2
Bello. exprè un altro che ho bisogno di familiarizzare. Mi piacciono molto le zshsoluzioni in generale (stavo solo leggendo del suo supporto per le sostituzioni nidificate anche sul lato sinistro di ${}ieri e desiderando shavere lo stesso), è solo un peccato che non sia sempre presente di default.
Jason C,

2
@JasonC - le informazioni contano di più. Rendi il meglio accessibile il più possibile - questo è comunque l'intero punto del sistema. Se il rappresentante acquistasse del cibo, potrei arrabbiarmi, ma più spesso (che mai) le informazioni portano a casa la pancetta
mikeserv

1
@mikeserv "Richiesta: scambia rappresentante per pancetta"; cerca meta qui vengo.
Jason C,

1
@mikerserv, il tuo è POSIX, utilizza solo i builtin e non esegue il fork di alcun processo. Non usare la sostituzione dei comandi significa anche evitare problemi con il trascinamento di nuove righe, quindi è anche una buona risposta.
Stéphane Chazelas,

1
@mikeserv, non intendevo implicare che nonexpr fosse POSIX. Certamente è. Tuttavia è raramente integrato.
Stéphane Chazelas,

13
var=123456
echo "${var#"${var%???}"}"

###OUTPUT###

456

Quello rimuove dapprima gli ultimi tre caratteri, $varquindi rimuove dai $varrisultati di quella rimozione - che restituisce gli ultimi tre caratteri di $var. Ecco alcuni esempi più specificamente volti a dimostrare come potresti fare una cosa del genere:

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

Non devi diffondere tutto questo attraverso così tanti comandi. Puoi compattarlo:

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

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

La combinazione $IFScon i setparametri di shell ting può anche essere un mezzo molto efficace per analizzare e perforare attraverso le variabili shell:

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

Che vi porterà solo i tre personaggi immediatamente precedente il primo periodo dopo l'ultimo /in $path. Se desideri recuperare solo i primi tre caratteri immediatamente precedenti l'ultimo .in $path (ad esempio, se esiste la possibilità di più di uno .nel nome file) :

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

In entrambi i casi puoi fare:

newvar=$(IFS...)

E...

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

... stamperà quanto segue .

Se non ti dispiace usare un programma esterno puoi fare:

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

Se esiste la possibilità di un \ncarattere ewline nel nome file (non applicabile per le soluzioni shell native - lo gestiscono comunque tutti) :

printf %s "${path##*/}" | sed 'H;$!d;g;s/.*\(...\)\..*/\1/'

1
Grazie. Ho anche trovato documentazione . Ma per ottenere gli ultimi 3 personaggi da $baselì, il meglio che potevo fare era la tre righe name=${var##*/} ; base=${name%%.*} ; lastpart=${base#${base%???}}. Tra i lati positivi è puro bash, ma è ancora 3 righe. (Nel tuo esempio di "/tmp/file.txt" avrei bisogno di "ile" anziché di "file".) Ho appena imparato molto sulla sostituzione dei parametri; Non avevo idea che potesse farlo ... abbastanza utile. Lo trovo molto leggibile, anche personalmente.
Jason C,

1
@JasonC - questo è un comportamento completamente portatile - non è specifico per bash. Consiglio di leggere questo .
Mikeserv,

1
Beh, immagino, posso usare %invece di %%rimuovere il suffisso, e in realtà non ho bisogno di spogliare il percorso, quindi posso ottenere una linea più bella di due noextn=${var%.*} ; lastpart=${noextn#${noextn%???}}.
Jason C,

1
@JasonC - sì, sembra che funzionerebbe. Si romperà se c'è $IFSdentro ${noextn}e non citate l'espansione. Quindi, questo è più sicuro:lastpart=${noextn#"${noextn%???}"}
mikeserv,

1
@JasonC - infine, se hai trovato utile quanto sopra, potresti dare un'occhiata a questo . Si occupa di altre forme di espansione dei parametri e anche le altre risposte a questa domanda sono davvero buone. E ci sono collegamenti ad altre due risposte sullo stesso argomento all'interno. Se vuoi.
Mikeserv,

4

Se puoi usare perl:

lastpart=$(
    perl -e 'print substr((split(/\.[^.]*$/,shift))[0], -3, 3)
            ' -- "$(basename -- "$1")"
)

è fantastico. ho ottenuto un nuovo voto.
Mikeserv,

Un po 'più conciso: perl -e 'shift =~ /(.{3})\.[^.]*$/ && print $1' $filename. basenameSarebbe necessario un ulteriore se il nome file potrebbe non contenere alcun suffisso ma alcune directory nel percorso lo fanno.
Dubu,

@Dubu: la soluzione fallisce sempre se il nome file non ha suffisso.
cuonglm,

1
@Gnouc Questo era di proposito. Ma hai ragione, questo potrebbe essere sbagliato a seconda dello scopo. Alternativa:perl -e 'shift =~ m#(.{3})(?:\.[^./]*)?$# && print $1' $filename
Dubu,

2

sed funziona per questo:

[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

Se il tuo sednon supporta -r, basta sostituire le istanze di ()con \(e \), quindi -rnon è necessario.


1

Se perl è disponibile, trovo che possa essere più leggibile di altre soluzioni, in particolare perché il suo linguaggio regex è più espressivo e ha il /xmodificatore, che consente di scrivere regex più chiari:

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

Questo non stampa nulla se non esiste tale corrispondenza (se il nome di base non ha estensione o se la radice prima dell'estensione è troppo corta). A seconda delle tue esigenze, puoi regolare regex. Questa regex applica i vincoli:

  1. Corrisponde ai 3 caratteri prima dell'estensione finale (la parte dopo e incluso l'ultimo punto). Questi 3 caratteri possono contenere un punto.
  2. L'estensione può essere vuota (tranne il punto).
  3. La parte corrispondente e l'estensione devono far parte del nome di base (la parte dopo l'ultima barra).

L'uso di questo in una sostituzione di comando ha i normali problemi con la rimozione di troppe newline finali, un problema che influenza anche la risposta di Stéphane. Può essere risolto in entrambi i casi, ma qui è un po 'più semplice:

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

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

0

Penso che questa funzione bash, pathStr (), farà quello che stai cercando.

Non richiede awk, sed, grep, perl o expr. Utilizza solo built-in bash quindi è abbastanza veloce.

Ho anche incluso le funzioni argsNumber e isOption dipendenti, ma le loro funzionalità potrebbero essere facilmente incorporate in pathStr.

La funzione dipendente ifHelpShow non è inclusa in quanto presenta numerose sottodipendenze per l'output del testo della guida nella riga di comando del terminale o in una finestra di dialogo della GUI tramite YAD . Il testo di aiuto passato ad esso è incluso per la documentazione. Avvisa se desideri ifHelpShow e le sue persone a carico.

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
}

RISORSE


Non capisco - è già stato dimostrato qui come fare tutto in modo completamente portatile - senza problemi bash- apparentemente più semplice di così. Inoltre, cos'è ${#@}?
Mikeserv,

Questo solo impacchetta la funzionalità in una funzione riutilizzabile. ri: $ {# @} ... La manipolazione delle matrici e dei loro elementi richiede la notazione della variabile completa $ {}. $ @ è la 'matrice' di argomenti. $ {# @} è la sintassi bash per il numero di argomenti.
DocSalvager,

No, $#è la sintassi per il numero di argomenti e viene utilizzata anche qui altrove.
Mikeserv,

Hai ragione nel dire che "$ #" è il sistema ampiamente documentato per "numero di argomenti". Tuttavia, ho appena verificato che "$ {# @}" è equivalente. L'ho risolto dopo aver sperimentato le differenze e le somiglianze tra argomenti posizionali e array. Il successivo deriva dalla sintassi dell'array che apparentemente è sinonimo della sintassi "$ #" più breve e semplice. Ho modificato e documentato argsNumber () per utilizzare "$ #". Grazie!
DocSalvager,

${#@}nella maggior parte dei casi non è equivalente: le specifiche POSIX indicano i risultati di eventuali espansioni di parametri su uno $@o entrambi $*, purtroppo, non sono specificati. Potrebbe funzionare bashma questa non è una funzione affidabile, immagino sia quello che sto cercando di dire.,
mikeserv,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.