bash - script - unix filename extension




Extrahiere Dateinamen und Erweiterung in Bash (20)

Ich möchte den Dateinamen (ohne Erweiterung) und die Erweiterung getrennt erhalten.

Die beste Lösung, die ich bisher gefunden habe, ist:

NAME=`echo "$FILE" | cut -d'.' -f1`
EXTENSION=`echo "$FILE" | cut -d'.' -f2`

Dies ist falsch, weil es nicht funktioniert, wenn der Dateiname mehrere "." Figuren. Wenn, sagen wir, ich habe abjs , wird es a und b.js , anstelle von ab und js betrachten .

Es kann leicht in Python mit gemacht werden

file, ext = os.path.splitext(path)

aber ich würde es vorziehen, keinen Python-Interpreter nur dafür, wenn möglich, zu feuern.

Irgendwelche besseren Ideen?


Magische Dateierkennung

Zusätzlich zu den vielen guten Antworten zu dieser Frage möchte ich hinzufügen:

Unter Linux und anderen Unixen gibt es einen magischen Befehl namens file , der die Dateityp-Erkennung durch Analyse einiger erster Bytes der Datei durchführt. Dies ist ein sehr altes Tool, das ursprünglich für Druckserver verwendet wurde (wenn nicht für ... erstellt, da bin ich mir nicht sicher).

file myfile.txt
myfile.txt: UTF-8 Unicode text

file -b --mime-type myfile.txt
text/plain

Standard-Erweiterungen finden sich in /etc/mime.types (auf meinem Debian GNU / Linux-Desktop. Siehe man file und man mime.types . Vielleicht musst du das file und mime-support man mime.types installieren):

grep $( file -b --mime-type myfile.txt ) </etc/mime.types
text/plain      asc txt text pot brf srt

Sie könnten eine bash Funktion zur Bestimmung der richtigen Erweiterung erstellen. Es gibt ein kleines (nicht perfektes) Beispiel:

file2ext() {
    local _mimetype=$(file -Lb --mime-type "$1") _line _basemimetype
    case ${_mimetype##*[/.-]} in
        gzip | bzip2 | xz | z )
            _mimetype=${_mimetype##*[/.-]}
            _mimetype=${_mimetype//ip}
            _basemimetype=$(file -zLb --mime-type "$1")
            ;;
        stream )
            _mimetype=($(file -Lb "$1"))
            [ "${_mimetype[1]}" = "compressed" ] &&
                _basemimetype=$(file -b --mime-type - < <(
                        ${_mimetype,,} -d <"$1")) ||
                _basemimetype=${_mimetype,,}
            _mimetype=${_mimetype,,}
            ;;
        executable )  _mimetype='' _basemimetype='' ;;
        dosexec )     _mimetype='' _basemimetype='exe' ;;
        shellscript ) _mimetype='' _basemimetype='sh' ;;
        * )
            _basemimetype=$_mimetype
            _mimetype=''
            ;;
    esac
    while read -a _line ;do
        if [ "$_line" == "$_basemimetype" ] ;then
            [ "$_line[1]" ] &&
                _basemimetype=${_line[1]} ||
                _basemimetype=${_basemimetype##*[/.-]}
            break
        fi
        done </etc/mime.types
    case ${_basemimetype##*[/.-]} in
        executable ) _basemimetype='' ;;
        shellscript ) _basemimetype='sh' ;;
        dosexec ) _basemimetype='exe' ;;
        * ) ;;
    esac
    [ "$_mimetype" ] && [ "$_basemimetype" != "$_mimetype" ] &&
      printf ${2+-v} $2 "%s.%s" ${_basemimetype##*[/.-]} ${_mimetype##*[/.-]} ||
      printf ${2+-v} $2 "%s" ${_basemimetype##*[/.-]}
}

Diese Funktion könnte eine Bash-Variable festlegen, die später verwendet werden kann:

(Dies ist inspiriert von @ Petesh richtige Antwort):

filename=$(basename "$fullfile")
filename="${filename%.*}"
file2ext "$fullfile" extension

echo "$fullfile -> $filename . $extension"

Das scheint nicht zu funktionieren, wenn die Datei keine Erweiterung oder keinen Dateinamen hat. Hier ist, was ich benutze; Es verwendet nur eingebaute und behandelt mehr (aber nicht alle) pathologische Dateinamen.

#!/bin/bash
for fullpath in "[email protected]"
do
    filename="${fullpath##*/}"                      # Strip longest match of */ from start
    dir="${fullpath:0:${#fullpath} - ${#filename}}" # Substring from 0 thru pos of filename
    base="${filename%.[^.]*}"                       # Strip shortest match of . plus at least one non-dot char from end
    ext="${filename:${#base} + 1}"                  # Substring from len of base thru end
    if [[ -z "$base" && -n "$ext" ]]; then          # If we have an extension and no base, it's really the base
        base=".$ext"
        ext=""
    fi

    echo -e "$fullpath:\n\tdir  = \"$dir\"\n\tbase = \"$base\"\n\text  = \"$ext\""
done

Und hier sind einige Testfälle:

$ basename-and-extension.sh / /home/me/ /home/me/file /home/me/file.tar /home/me/file.tar.gz /home/me/.hidden /home/me/.hidden.tar /home/me/.. .
/:
    dir  = "/"
    base = ""
    ext  = ""
/home/me/:
    dir  = "/home/me/"
    base = ""
    ext  = ""
/home/me/file:
    dir  = "/home/me/"
    base = "file"
    ext  = ""
/home/me/file.tar:
    dir  = "/home/me/"
    base = "file"
    ext  = "tar"
/home/me/file.tar.gz:
    dir  = "/home/me/"
    base = "file.tar"
    ext  = "gz"
/home/me/.hidden:
    dir  = "/home/me/"
    base = ".hidden"
    ext  = ""
/home/me/.hidden.tar:
    dir  = "/home/me/"
    base = ".hidden"
    ext  = "tar"
/home/me/..:
    dir  = "/home/me/"
    base = ".."
    ext  = ""
.:
    dir  = ""
    base = "."
    ext  = ""

Hier ist Code mit AWK . Es kann einfacher gemacht werden. Aber ich bin nicht gut in AWK.

filename$ ls
abc.a.txt  a.b.c.txt  pp-kk.txt
filename$ find . -type f | awk -F/ '{print $2}' | rev | awk -F"." '{$1="";print}' | rev | awk 'gsub(" ",".") ,sub(".$", "")'
abc.a
a.b.c
pp-kk
filename$ find . -type f | awk -F/ '{print $2}' | awk -F"." '{print $NF}'
txt
txt
txt

Hier sind einige alternative Vorschläge (hauptsächlich in awk ), einschließlich einiger fortgeschrittener Anwendungsfälle, wie das Extrahieren von Versionsnummern für Softwarepakete.

f='/path/to/complex/file.1.0.1.tar.gz'

# Filename : 'file.1.0.x.tar.gz'
    echo "$f" | awk -F'/' '{print $NF}'

# Extension (last): 'gz'
    echo "$f" | awk -F'[.]' '{print $NF}'

# Extension (all) : '1.0.1.tar.gz'
    echo "$f" | awk '{sub(/[^.]*[.]/, "", $0)} 1'

# Extension (last-2): 'tar.gz'
    echo "$f" | awk -F'[.]' '{print $(NF-1)"."$NF}'

# Basename : 'file'
    echo "$f" | awk '{gsub(/.*[/]|[.].*/, "", $0)} 1'

# Basename-extended : 'file.1.0.1.tar'
    echo "$f" | awk '{gsub(/.*[/]|[.]{1}[^.]+$/, "", $0)} 1'

# Path : '/path/to/complex/'
    echo "$f" | awk '{match($0, /.*[/]/, a); print a[0]}'
    # or 
    echo "$f" | grep -Eo '.*[/]'

# Folder (containing the file) : 'complex'
    echo "$f" | awk -F'/' '{$1=""; print $(NF-1)}'

# Version : '1.0.1'
    # Defined as 'number.number' or 'number.number.number'
    echo "$f" | grep -Eo '[0-9]+[.]+[0-9]+[.]?[0-9]?'

    # Version - major : '1'
    echo "$f" | grep -Eo '[0-9]+[.]+[0-9]+[.]?[0-9]?' | cut -d. -f1

    # Version - minor : '0'
    echo "$f" | grep -Eo '[0-9]+[.]+[0-9]+[.]?[0-9]?' | cut -d. -f2

    # Version - patch : '1'
    echo "$f" | grep -Eo '[0-9]+[.]+[0-9]+[.]?[0-9]?' | cut -d. -f3

# All Components : "path to complex file 1 0 1 tar gz"
    echo "$f" | awk -F'[/.]' '{$1=""; print $0}'

# Is absolute : True (exit-code : 0)
    # Return true if it is an absolute path (starting with '/' or '~/'
    echo "$f" | grep -q '^[/]\|^~/'

Alle Anwendungsfälle verwenden den ursprünglichen vollständigen Pfad als Eingabe, ohne von Zwischenergebnissen abhängig zu sein.


Ich denke, wenn Sie nur den Namen der Datei benötigen, können Sie Folgendes versuchen:

FULLPATH=/usr/share/X11/xorg.conf.d/50-synaptics.conf

# Remove all the prefix until the "/" character
FILENAME=${FULLPATH##*/}

# Remove all the prefix until the "." character
FILEEXTENSION=${FILENAME##*.}

# Remove a suffix, in our case, the filename. This will return the name of the directory that contains this file.
BASEDIRECTORY=${FULLPATH%$FILENAME}

echo "path = $FULLPATH"
echo "file name = $FILENAME"
echo "file extension = $FILEEXTENSION"
echo "base directory = $BASEDIRECTORY"

Und das ist alles = D.


Keine Notwendigkeit, mit awk oder sed oder sogar perl für diese einfache Aufgabe zu beschäftigen. Es gibt eine pure-Bash, os.path.splitext() kompatible Lösung, die nur Parametererweiterungen verwendet.

Referenzimplementierung

Dokumentation von os.path.splitext(path) :

Teilen Sie den Pfad Pfad in ein Paar (root, ext) so dass root + ext == path , und ext ist leer oder beginnt mit einem Punkt und enthält höchstens eine Periode. Vorlaufende Perioden auf dem Basisnamen werden ignoriert; splitext('.cshrc') zurück ('.cshrc', '') .

Python-Code:

root, ext = os.path.splitext(path)

Bash-Implementierung

Leitende Zeiten ehren

root="${path%.*}"
ext="${path#"$root"}"

Vorperioden ignorieren

root="${path#.}";root="${path%"$root"}${root%.*}"
ext="${path#"$root"}"

Tests

Hier finden Sie Testfälle für die Implementierung von Ignoring leading periods , die bei jeder Eingabe mit der Python-Referenzimplementierung übereinstimmen sollten.

|---------------|-----------|-------|
|path           |root       |ext    |
|---------------|-----------|-------|
|' .txt'        |' '        |'.txt' |
|' .txt.txt'    |' .txt'    |'.txt' |
|' txt'         |' txt'     |''     |
|'*.txt.txt'    |'*.txt'    |'.txt' |
|'.cshrc'       |'.cshrc'   |''     |
|'.txt'         |'.txt'     |''     |
|'?.txt.txt'    |'?.txt'    |'.txt' |
|'\n.txt.txt'   |'\n.txt'   |'.txt' |
|'\t.txt.txt'   |'\t.txt'   |'.txt' |
|'a b.txt.txt'  |'a b.txt'  |'.txt' |
|'a*b.txt.txt'  |'a*b.txt'  |'.txt' |
|'a?b.txt.txt'  |'a?b.txt'  |'.txt' |
|'a\nb.txt.txt' |'a\nb.txt' |'.txt' |
|'a\tb.txt.txt' |'a\tb.txt' |'.txt' |
|'txt'          |'txt'      |''     |
|'txt.pdf'      |'txt'      |'.pdf' |
|'txt.tar.gz'   |'txt.tar'  |'.gz'  |
|'txt.txt'      |'txt'      |'.txt' |
|---------------|-----------|-------|

Testergebnisse

Alle Tests bestanden.


Mellen schreibt in einem Kommentar zu einem Blogbeitrag:

Mit Bash gibt es auch ${file%.*} , Um den Dateinamen ohne die Erweiterung und ${file##*.} Zu erhalten, um die Erweiterung alleine zu erhalten. Das ist,

file="thisfile.txt"
echo "filename: ${file%.*}"
echo "extension: ${file##*.}"

Ausgänge:

filename: thisfile
extension: txt

Nennen Sie zuerst den Dateinamen ohne den Pfad:

filename=$(basename -- "$fullfile")
extension="${filename##*.}"
filename="${filename%.*}"

Alternativ können Sie sich auf das letzte '/' des Pfads anstelle des '.' was auch dann funktionieren sollte, wenn Sie unvorhersehbare Dateierweiterungen haben:

filename="${fullfile##*/}"

Ok, wenn ich das richtig verstanden habe, ist das Problem hier, wie man den Namen und die vollständige Erweiterung einer Datei bekommt, die mehrere Erweiterungen hat, zB stuff.tar.gz .

Das funktioniert für mich:

fullfile="stuff.tar.gz"
fileExt=${fullfile#*.}
fileName=${fullfile%*.$fileExt}

Dies gibt dir stuff als Dateiname und .tar.gz als Erweiterung. Es funktioniert für eine beliebige Anzahl von Erweiterungen, einschließlich 0. Hoffe das hilft für jeden mit dem gleichen Problem =)


Sie können basename .

Beispiel:

$ basename foo-bar.tar.gz .tar.gz
foo-bar

Sie müssen basename mit der Erweiterung angeben, die entfernt werden soll. Wenn Sie jedoch immer tar mit -z ausführen, wissen Sie, dass die Erweiterung .tar.gz .

Dies sollte tun, was Sie wollen:

tar -zxvf $1
cd $(basename $1 .tar.gz)

Sie können den Schnitt erzwingen, um alle Felder anzuzeigen, und die nachfolgenden, um die Feldnummer hinzuzufügen.

NAME=`basename "$FILE"`
EXTENSION=`echo "$NAME" | cut -d'.' -f2-`

Wenn FILE also eth0.pcap.gz , wird die EXTENSION pcap.gz

Mit derselben Logik können Sie den Dateinamen auch mit '-' mit Schnitt wie folgt abrufen:

NAME=`basename "$FILE" | cut -d'.' -f-1`

Dies funktioniert sogar für Dateinamen, die keine Erweiterung haben.


Sie können die Magie von POSIX-Variablen verwenden:

bash-3.2$ FILENAME=somefile.tar.gz
bash-3.2$ echo ${FILENAME%%.*}
somefile
bash-3.2$ echo ${FILENAME%.*}
somefile.tar

Wenn Ihr Dateiname das Format ./somefile.tar.gz dann würde echo ${FILENAME%%.*} Die längste Übereinstimmung gierig entfernen . und du hättest die leere Zeichenfolge.

(Sie können das mit einer temporären Variable umgehen:

FULL_FILENAME=$FILENAME
FILENAME=${FULL_FILENAME##*/}
echo ${FILENAME%%.*}

)

Diese site erklärt mehr.

${variable%pattern}
  Trim the shortest match from the end
${variable##pattern}
  Trim the longest match from the beginning
${variable%%pattern}
  Trim the longest match from the end
${variable#pattern}
  Trim the shortest match from the beginning

So extrahieren Sie den Dateinamen und die Erweiterung in fish :

function split-filename-extension --description "Prints the filename and extension"
  for file in $argv
    if test -f $file
      set --local extension (echo $file | awk -F. '{print $NF}')
      set --local filename (basename $file .$extension)
      echo "$filename $extension"
    else
      echo "$file is not a valid file"
    end
  end
end

Caveats: Splits auf dem letzten Punkt, was gut für Dateinamen mit Punkten in ihnen funktioniert, aber nicht gut für Erweiterungen mit Punkten in ihnen. Siehe Beispiel unten.

Verwendung:

$ split-filename-extension foo-0.4.2.zip bar.tar.gz
foo-0.4.2 zip  # Looks good!
bar.tar gz  # Careful, you probably want .tar.gz as the extension.

Es gibt wahrscheinlich bessere Möglichkeiten, dies zu tun. Fühlen Sie sich frei, meine Antwort zu bearbeiten, um sie zu verbessern.

Wenn es eine begrenzte Anzahl von Erweiterungen gibt, mit denen Sie es zu tun haben und Sie alle kennen, versuchen Sie Folgendes:

switch $file
  case *.tar
    echo (basename $file .tar) tar
  case *.tar.bz2
    echo (basename $file .tar.bz2) tar.bz2
  case *.tar.gz
    echo (basename $file .tar.gz) tar.gz
  # and so on
end

Dies hat nicht den Vorbehalt wie das erste Beispiel, aber Sie müssen jeden Fall behandeln, so dass es mühsamer sein könnte, abhängig davon, wie viele Erweiterungen Sie erwarten können.


Wenn nur der Dateiname benötigt wird, können sowohl der Pfad als auch die Erweiterung in einer einzelnen Zeile entfernt werden.

filename=$(basename ${fullname%.*})

Based largely off of @mklement0's excellent, and chock-full of random, useful bashisms - as well as other answers to this / other questions / "that darn internet"... I wrapped it all up in a little, slightly more comprehensible, reusable function for my (or your) .bash_profile that takes care of what (I consider) should be a more robust version of dirname / basename / what have you ..

function path { SAVEIFS=$IFS; IFS=""   # stash IFS for safe-keeping, etc.
    [[ $# != 2 ]] && echo "usage: path <path> <dir|name|fullname|ext>" && return    # demand 2 arguments
    [[ $1 =~ ^(.*/)?(.+)?$ ]] && {     # regex parse the path
        dir=${BASH_REMATCH[1]}
        file=${BASH_REMATCH[2]}
        ext=$([[ $file = *.* ]] && printf %s ${file##*.} || printf '')
        # edge cases for extesionless files and files like ".nesh_profile.coffee"
        [[ $file == $ext ]] && fnr=$file && ext='' || fnr=${file:0:$((${#file}-${#ext}))}
        case "$2" in
             dir) echo      "${dir%/*}"; ;;
            name) echo      "${fnr%.*}"; ;;
        fullname) echo "${fnr%.*}.$ext"; ;;
             ext) echo           "$ext"; ;;
        esac
    }
    IFS=$SAVEIFS
}     

Usage examples...

SOMEPATH=/path/to.some/.random\ file.gzip
path $SOMEPATH dir        # /path/to.some
path $SOMEPATH name       # .random file
path $SOMEPATH ext        # gzip
path $SOMEPATH fullname   # .random file.gzip                     
path gobbledygook         # usage: -bash <path> <dir|name|fullname|ext>

From the answers above, the shortest oneliner to mimic Python's

file, ext = os.path.splitext(path)

presuming your file really does have an extension, is

EXT="${PATH##*.}"; FILE=$(basename "$PATH" .$EXT)

If you also want to allow empty extensions, this is the shortest I could come up with:

echo 'hello.txt' | sed -r 's/.+\.(.+)|.*/\1/' # EXTENSION
echo 'hello.txt' | sed -r 's/(.+)\..+|(.*)/\1\2/' # FILENAME

1st line explained: It matches PATH.EXT or ANYTHING and replaces it with EXT. If ANYTHING was matched, the ext group is not captured.


In order to make dir more useful (in the case a local file with no path is specified as input) I did the following:

# Substring from 0 thru pos of filename
dir="${fullpath:0:${#fullpath} - ${#filename}}"
if [[ -z "$dir" ]]; then
    dir="./"
fi

This allows you to do something useful like add a suffix to the input file basename as:

outfile=${dir}${base}_suffix.${ext}

testcase: foo.bar
dir: "./"
base: "foo"
ext: "bar"
outfile: "./foo_suffix.bar"

testcase: /home/me/foo.bar
dir: "/home/me/"
base: "foo"
ext: "bar"
outfile: "/home/me/foo_suffix.bar"

$ F = "text file.test.txt"  
$ echo ${F/*./}  
txt  

Dies stellt mehrere Punkte und Leerzeichen in einem Dateinamen bereit. Wenn jedoch keine Erweiterung vorhanden ist, wird der Dateiname selbst zurückgegeben. Einfach zu überprüfen, obwohl; Testen Sie einfach, ob der Dateiname und die Erweiterung identisch sind.

Natürlich funktioniert diese Methode nicht für .tar.gz Dateien. Dies könnte jedoch in einem zweistufigen Prozess behandelt werden. Wenn die Erweiterung gz ist, überprüfen Sie erneut, ob es auch eine Teer-Erweiterung gibt.


pax> echo a.b.js | sed 's/\.[^.]*$//'
a.b
pax> echo a.b.js | sed 's/^.*\.//'
js

funktioniert gut, so können Sie einfach verwenden:

pax> FILE=a.b.js
pax> NAME=$(echo "$FILE" | sed 's/\.[^.]*$//')
pax> EXTENSION=$(echo "$FILE" | sed 's/^.*\.//')
pax> echo $NAME
a.b
pax> echo $EXTENSION
js

Die Befehle funktionieren übrigens wie folgt.

Der Befehl für NAME ersetzt ein "." Zeichen gefolgt von einer beliebigen Anzahl von Nicht- "." Zeichen bis zum Ende der Zeile, mit nichts (dh es entfernt alles vom endgültigen "." bis zum Ende der Zeile, inklusive). Dies ist im Grunde eine nicht-gierige Ersetzung mit Regex Tricks.

Der Befehl für EXTENSION ersetzt eine beliebige Anzahl von Zeichen gefolgt von einem "." Zeichen am Anfang der Zeile, mit nichts (dh es entfernt alles vom Anfang der Zeile bis zum letzten Punkt, inklusive). Dies ist eine gierige Ersetzung, die die Standardaktion ist.







filenames