Kürzeste Methode zum Extrahieren der letzten 3 Zeichen des Basisdateinamens (minus Suffix)

12

Ich versuche, eine Variable in einem sh-Skript auf die letzten 3 Zeichen des Basisnamens einer Datei zu setzen (mit Basisname meine ich ohne Pfad und ohne Suffix). Ich habe es geschafft, aber ich frage mich, ob es einen kürzeren, einzelnen Befehl gibt, den ich aus Neugier verwenden kann. Ursprünglich hatte ich ein Einzeiler mit awk, aber es war ziemlich lang. Derzeit habe ich dieses zweizeilige Skript (unter der Annahme, dass sich ein vollständiger Dateiname in befindet $1):

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

So endet beispielsweise "/path/to/somefile.txt" mit "ile" in $lastpart.

Kann ich irgendwie kombinieren basenameund das Bit, um das Suffix in einem einzigen Befehl zu trennen, und gibt es eine Möglichkeit, es an tail(oder etwas anderes, das ich verwenden kann) zu senden, ohne eine Pipe zu verwenden? Das Suffix ist unbekannt, daher kann ich es nicht als Parameter verwenden basename.

Das Hauptziel ist nicht, so kurz wie möglich zu sein, sondern auf einen Blick so gut wie möglich lesbar zu sein. Der eigentliche Kontext all dessen ist diese Frage zum Superuser , bei der ich versuche, eine einigermaßen einfache Antwort zu finden.

Jason C
quelle
2
Wie würden Sie mit einer Datei umgehen file.one.two.three? Möchtest du ileoder two?
Terdon
@terdon twowürde funktionieren; die verlängerung dazu wäre wohl .threeich.
Jason C

Antworten:

6

Das ist ein typischer Job für expr:

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

Wenn Sie wissen, dass Ihre Dateinamen das erwartete Format haben (enthält nur einen Punkt und mindestens 3 Zeichen vor dem Punkt), können Sie Folgendes vereinfachen:

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

Beachten Sie, dass der Beendigungsstatus ungleich Null ist, wenn keine Übereinstimmung vorliegt, aber auch, wenn der übereinstimmende Teil eine Zahl ist, die auf 0 aufgelöst wird (wie für a000.txtoder a-00.txt).

Mit zsh:

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

( :tfür Schwanz (Basisname), :rfür Rest (mit entfernter Erweiterung)).

Stéphane Chazelas
quelle
2
Nett. exprist eine andere, mit der ich mich vertraut machen muss. Ich wirklich wie die zshLösungen im Allgemeinen (ich lese gerade über seine Unterstützung für verschachtelte Substitutionen auf der linken Seite eines ${}gestern zu und wünschte shdas gleiche habe), es ist nur ein doofer , dass es nicht immer standardmäßig vorhanden ist.
Jason C
2
@JasonC - die Informationen sind am wichtigsten. Machen Sie das Beste daraus so zugänglich wie möglich - das ist sowieso der springende Punkt des Systems. Wenn der Reiseleiter Essen gekauft hätte, wäre ich vielleicht verärgert, aber öfter (als nie) bringt die Info den Speck nach Hause
mikeserv
1
@mikeserv "Anfrage: Vertreter gegen Speck eintauschen"; pass auf meta hier komm ich.
Jason C
1
Bei @mikerserv handelt es sich um POSIX, es werden nur integrierte Funktionen verwendet und es werden keine Prozesse gespalten. Wenn Sie die Befehlssubstitution nicht verwenden, vermeiden Sie auch Probleme mit nachgestellten Zeilenumbrüchen. Daher ist dies auch eine gute Antwort.
Stéphane Chazelas
1
@mikeserv, ich wollte nicht implizieren, exprwar nicht POSIX. Es ist sicherlich. Es ist jedoch selten eingebaut.
Stéphane Chazelas
13
var=123456
echo "${var#"${var%???}"}"

###OUTPUT###

456

Das erste Mal werden die letzten drei Zeichen $varentfernt, danach werden $vardie Ergebnisse dieser Entfernung entfernt. Dabei werden die letzten drei Zeichen von zurückgegeben $var. Hier sind einige Beispiele, die speziell zeigen sollen, wie Sie so etwas tun können:

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

Sie müssen dies nicht durch so viele Befehle verbreiten. Sie können dies komprimieren:

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

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

Die Kombination $IFSmit setting-Shell-Parametern kann auch ein sehr effektives Mittel zum Parsen und Durchsuchen von Shell-Variablen sein:

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

Dadurch erhalten Sie nur die drei Zeichen, die unmittelbar vor dem ersten Punkt nach dem letzten /in stehen $path. Wenn Sie nur die ersten drei Zeichen unmittelbar vor dem letzten .in abrufen möchten$path (zum Beispiel, wenn der .Dateiname möglicherweise mehrere ) :

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

In beiden Fällen können Sie Folgendes tun:

newvar=$(IFS...)

Und...

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

... druckt was folgt .

Wenn es Ihnen nichts ausmacht, ein externes Programm zu verwenden, können Sie Folgendes tun:

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

Wenn es die Möglichkeit gibt, dass \nder Dateiname ein ewline-Zeichen enthält (gilt nicht für native Shell-Lösungen - das erledigen sowieso alle) :

printf %s "${path##*/}" | sed 'H;$!d;g;s/.*\(...\)\..*/\1/'
mikeserv
quelle
1
Es ist, danke. Ich habe auch Dokumentation gefunden . Aber um die letzten drei Zeichen von $basedort zu bekommen, war das Beste, was ich tun konnte, die drei Zeilen name=${var##*/} ; base=${name%%.*} ; lastpart=${base#${base%???}}. Auf der positiven Seite ist es reine Bash, aber es sind immer noch 3 Zeilen. (In Ihrem Beispiel für "/tmp/file.txt" benötige ich "ile" anstelle von "file".) Ich habe gerade viel über das Ersetzen von Parametern gelernt. Ich hatte keine Ahnung, dass es das tun könnte ... ziemlich praktisch. Ich finde es auch persönlich sehr lesbar.
Jason C
1
@JasonC - dies ist ein vollständig portierbares Verhalten - es ist nicht bash-spezifisch. Ich empfehle die Lektüre dieses .
mikeserv
1
Nun, ich schätze, ich kann das Suffix entfernen, %anstatt es %%zu verwenden, und ich muss den Pfad eigentlich nicht entfernen, damit ich eine schönere, zweizeilige Linie bekomme noextn=${var%.*} ; lastpart=${noextn#${noextn%???}}.
Jason C
1
@JasonC - ja, das sieht so aus, als würde es funktionieren. Es wird brechen , wenn es $IFSin ${noextn}und Sie zitieren nicht die Erweiterung. Das ist also sicherer:lastpart=${noextn#"${noextn%???}"}
mikeserv
1
@JasonC - zuletzt, wenn Sie die oben hilfreich, könnten Sie wollen , betrachten dies . Es geht um andere Formen der Parametererweiterung, und die anderen Antworten auf diese Frage sind auch wirklich gut. Und es gibt Links zu zwei anderen Antworten zum selben Thema. Falls Sie es wollen.
mikeserv
4

Wenn Sie verwenden können perl:

lastpart=$(
    perl -e 'print substr((split(/\.[^.]*$/,shift))[0], -3, 3)
            ' -- "$(basename -- "$1")"
)
cuonglm
quelle
Das ist cool. habe keine Stimme bekommen.
mikeserv
Ein bisschen prägnanter: perl -e 'shift =~ /(.{3})\.[^.]*$/ && print $1' $filename. Ein zusätzlichesbasename wäre erforderlich, wenn der Dateiname möglicherweise kein Suffix enthält, jedoch ein Verzeichnis im Pfad.
Dubu
@ Dubu: Ihre Lösung schlägt immer fehl, wenn der Dateiname kein Suffix hat.
Donnerstag,
1
@ Gnouc Dies war absichtlich. Aber du hast recht, das könnte je nach Zweck falsch sein. Alternative:perl -e 'shift =~ m#(.{3})(?:\.[^./]*)?$# && print $1' $filename
Dubu
2

sed funktioniert dafür:

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

Oder

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

Wenn dein sed nicht unterstützt -r, ersetzen Sie einfach die Instanzen ()mit \(und \), und dann -rist nicht erforderlich.

BenjiWiebe
quelle
1

Wenn Perl verfügbar ist, kann es besser lesbar sein als andere Lösungen, insbesondere weil die Regex-Sprache aussagekräftiger ist und der /xModifikator das Schreiben klarerer Regex- Zeichen ermöglicht:

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

Dies gibt nichts aus, wenn es keine solche Übereinstimmung gibt (wenn der Basisname keine Erweiterung hat oder wenn der Stamm vor der Erweiterung zu kurz ist). Je nach Ihren Anforderungen können Sie die Regex anpassen. Dieser reguläre Ausdruck erzwingt die folgenden Einschränkungen:

  1. Es entspricht den 3 Zeichen vor der Enderweiterung (dem Teil nach und einschließlich des letzten Punkts). Diese 3 Zeichen können einen Punkt enthalten.
  2. Die Erweiterung kann leer sein (außer dem Punkt).
  3. Das übereinstimmende Teil und die Erweiterung müssen Teil des Basisnamens sein (der Teil nach dem letzten Schrägstrich).

Dies in einer Befehlsersetzung zu verwenden, hat die normalen Probleme, zu viele nachgestellte Zeilenumbrüche zu entfernen, ein Problem, das sich auch auf Stéphanes Antwort auswirkt. Es kann in beiden Fällen behandelt werden, ist aber hier etwas einfacher:

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

Ich denke, diese Bash-Funktion, pathStr (), wird das tun, wonach Sie suchen.

Es erfordert weder awk, sed, grep, perl noch expr. Es werden nur Bash-Buildins verwendet, daher ist es recht schnell.

Ich habe auch die abhängigen Funktionen argsNumber und isOption eingefügt, aber ihre Funktionen könnten leicht in pathStr integriert werden.

Die abhängige Funktion ifHelpShow ist nicht enthalten, da sie zahlreiche Abhängigkeiten für die Ausgabe des Hilfetextes über die Terminalbefehlszeile oder über YAD in ein GUI-Dialogfeld aufweist . Der übergebene Hilfetext dient der Dokumentation. Geben Sie an, ob Sie ifHelpShow und seine abhängigen Personen möchten.

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
}

RESSOURCEN

DocSalvager
quelle
Ich verstehe nicht - es wurde hier bereits vorgeführt, wie man Ähnliches vollständig portabel - ohne bashIsmen - anscheinend einfacher als dies macht. Was ist das auch ${#@}?
mikeserv
Dadurch wird die Funktionalität nur in eine wiederverwendbare Funktion verpackt. re: $ {# @} ... Für das Manipulieren von Arrays und ihren Elementen ist die vollständige Variablennotation $ {} erforderlich. $ @ ist das 'Array' von Argumenten. $ {# @} ist die Bash-Syntax für die Anzahl der Argumente.
DocSalvager
Nein, $#die Syntax für die Anzahl der Argumente wird angegeben und hier auch an anderer Stelle verwendet.
mikeserv
Sie haben Recht, dass "$ #" der allgemein dokumentierte Systax für "Anzahl der Argumente" ist. Ich habe jedoch gerade bestätigt, dass "$ {# @}" äquivalent ist. Nachdem ich mit den Unterschieden und Ähnlichkeiten zwischen Positionsargumenten und Arrays experimentiert hatte, endete ich damit. Das letztere kommt von der Array-Syntax, die anscheinend ein Synonym für die kürzere, einfachere "$ #" -Syntax ist. Ich habe argsNumber () geändert und dokumentiert, um "$ #" zu verwenden. Vielen Dank!
DocSalvager
${#@}ist in den meisten Fällen nicht äquivalent - die POSIX-Spezifikation gibt die Ergebnisse von Parametererweiterungen an $@oder $*ist leider nicht spezifiziert. Es kann funktionieren, bashaber das ist keine zuverlässige Funktion, ich denke, das ist, was ich versuche zu sagen.
mikeserv