Najkrótszy sposób na wyodrębnienie ostatnich 3 znaków podstawowej nazwy pliku (minus sufiks)

12

Próbuję ustawić zmienną w skrypcie sh na 3 ostatnie znaki podstawowej nazwy pliku (pod nazwą podstawową mam na myśli bez ścieżki i sufiksu). Udało mi się to zrobić, ale z czystej ciekawości zastanawiam się, czy mogę użyć krótszego pojedynczego polecenia. Początkowo miałam jedną wkładkę awk, ale była raczej długa. Obecnie mam ten dwuwierszowy skrypt (zakładając, że jest pełna nazwa pliku $1):

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

Na przykład „/path/to/somefile.txt” kończy się na „ile” w $lastpart.

Czy mogę jakoś połączyć basenamei nieco rozebrać sufiks w jednym poleceniu i czy istnieje sposób na wysłanie go do tail(lub czegoś innego, z czego mogę skorzystać) bez użycia potoku? Przyrostek jest nieznany, więc nie mogę oprzeć go na parametrze basename.

Głównym celem nie jest bycie tak krótkim, jak to tylko możliwe, aby być jak najbardziej czytelnym na pierwszy rzut oka. Rzeczywistym kontekstem tego wszystkiego jest pytanie Superuser , w którym staram się znaleźć dość prostą odpowiedź.

Jason C.
źródło
2
Jak poradziłbyś sobie z takim plikiem file.one.two.three? Chcesz ileczy two?
terdon
@terdon twodziałałby; Wydaje mi się, że to rozszerzenie .three.
Jason C

Odpowiedzi:

6

To typowa praca dla expr:

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

Jeśli wiesz, że nazwy plików mają oczekiwany format (zawiera jedną i tylko jedną kropkę oraz co najmniej 3 znaki przed kropką), można to uprościć w celu:

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

Pamiętaj, że stan wyjścia będzie różny od zera, jeśli nie ma dopasowania, ale także jeśli dopasowana część jest liczbą, która jest rozwiązywana do 0. (jak dla a000.txtlub a-00.txt)

Z zsh:

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

( :tdla ogona (basename), :rdla odpoczynku (z usuniętym przedłużeniem)).

Stéphane Chazelas
źródło
2
Ładny. exprto kolejny, z którym muszę się zapoznać. I naprawdę jak zshrozwiązania w ogóle (ja po prostu czytać o jego wsparcie dla zagnieżdżonych substytucji na lewym boku ${}wczoraj też i pragnąc shmiał takie same), to po prostu porażka, że nie zawsze jest obecny domyślnie.
Jason C
2
@JasonC - informacje są najważniejsze. Wykorzystaj to, co najlepsze, tak łatwo, jak to możliwe - i tak jest to cały punkt systemu. Jeśli przedstawiciel kupił jedzenie, może się zdenerwować, ale częściej (niż nigdy) informacje przynoszą do domu bekon
Mikeserv
1
@mikeserv "Request: Exchange rep for beacon"; uważaj meta, nadchodzę.
Jason C
1
@ mikerserv, twój jest POSIX, używa tylko wbudowanych i nie rozwidla żadnego procesu. Niestosowanie zastępowania poleceń oznacza również, że unikasz problemów z końcowymi znakami nowej linii, więc jest to również dobra odpowiedź.
Stéphane Chazelas
1
@ Mikeserv, nie chciałem sugerować, że exprto nie POSIX. To z pewnością jest. Jednak rzadko jest wbudowany.
Stéphane Chazelas
13
var=123456
echo "${var#"${var%???}"}"

###OUTPUT###

456

To najpierw usuwa ostatnie trzy znaki, $vara następnie usuwa z $varwyników tego usunięcia - co zwraca ostatnie trzy znaki $var. Oto kilka przykładów, których celem jest pokazanie, jak możesz zrobić coś takiego:

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

Nie musisz rozpowszechniać tego wszystkiego za pomocą tak wielu poleceń. Możesz to skompaktować:

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

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

Łączenie $IFSz setparametrami powłoki ting może być również bardzo skutecznym sposobem analizowania i drążenia zmiennych zmiennych powłoki:

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

Dzięki temu otrzymasz tylko trzy postacie bezpośrednio poprzedzające pierwszy okres po ostatnim /w $path. Jeśli chcesz pobrać tylko trzy pierwsze znaki bezpośrednio poprzedzające ostatnie .w $path (na przykład, jeśli istnieje możliwość użycia więcej niż jednego .w nazwie pliku) :

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

W obu przypadkach możesz zrobić:

newvar=$(IFS...)

I...

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

... wydrukuje to, co następuje .

Jeśli nie masz nic przeciwko korzystaniu z zewnętrznego programu, możesz:

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

Jeśli \nw nazwie pliku istnieje szansa na znak ewline (nie dotyczy rodzimych rozwiązań powłoki - wszystkie one i tak sobie radzą) :

printf %s "${path##*/}" | sed 'H;$!d;g;s/.*\(...\)\..*/\1/'
mikeserv
źródło
1
Dziękuję. Znalazłem również dokumentację . Ale aby uzyskać $basestamtąd ostatnie 3 znaki , najlepsze, co mogłem zrobić, to trzywiersz name=${var##*/} ; base=${name%%.*} ; lastpart=${base#${base%???}}. Na plus to czysta walka, ale wciąż 3 linie. (W twoim przykładzie „/tmp/file.txt” potrzebowałbym „ile” zamiast „file”.) Po prostu wiele się nauczyłem o podstawianiu parametrów; Nie miałem pojęcia, że ​​może to zrobić ... całkiem przydatne. Osobiście uważam to za bardzo czytelne.
Jason C,
1
@JasonC - jest to w pełni przenośne zachowanie - nie jest specyficzne dla bash. Polecam to przeczytać .
mikeserv
1
Cóż, myślę, że mogę użyć %zamiast %%usunąć sufiks i tak naprawdę nie muszę odcinać ścieżki, więc mogę uzyskać ładniejszą, dwie linie noextn=${var%.*} ; lastpart=${noextn#${noextn%???}}.
Jason C,
1
@JasonC - tak, wygląda na to, że zadziałałoby. Zepsuje się, jeśli jest $IFSw środku ${noextn}i nie zacytujesz rozszerzenia. Jest to więc bezpieczniejsze:lastpart=${noextn#"${noextn%???}"}
mikeserv
1
@JasonC - ostatni, jeśli stwierdzono wyżej pomocne, to może warto spojrzeć na to . Zajmuje się innymi formami rozszerzania parametrów, a inne odpowiedzi na to pytanie też są naprawdę dobre. I są linki do dwóch innych odpowiedzi na ten sam temat. Jeśli chcesz.
mikeserv
4

Jeśli możesz użyć perl:

lastpart=$(
    perl -e 'print substr((split(/\.[^.]*$/,shift))[0], -3, 3)
            ' -- "$(basename -- "$1")"
)
Cuonglm
źródło
to jest fajne. dostałem nowy głos.
mikeserv
Nieco bardziej zwięzły: perl -e 'shift =~ /(.{3})\.[^.]*$/ && print $1' $filename. Potrzebne basenamebędą dodatkowe , jeśli nazwa pliku może nie zawierać sufiksu, ale zawiera katalog na ścieżce.
Dubu
@Dubu: Twoje rozwiązanie zawsze kończy się niepowodzeniem, jeśli nazwa pliku nie ma sufiksu.
cuonglm
1
@Gnouc To było zamierzone. Ale masz rację, może być źle w zależności od celu. Alternatywa:perl -e 'shift =~ m#(.{3})(?:\.[^./]*)?$# && print $1' $filename
Dubu,
2

sed działa na to:

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

Lub

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

Jeśli urządzenie sednie obsługuje -r, po prostu zastąpić wystąpień ()z \(a \), a następnie -rnie jest potrzebne.

BenjiWiebe
źródło
1

Jeśli perl jest dostępny, uważam, że może być bardziej czytelny niż inne rozwiązania, szczególnie dlatego, że jego język wyrażeń regularnych jest bardziej ekspresyjny i ma /xmodyfikator, który pozwala na pisanie wyraźniejszych wyrażeń regularnych :

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

Nie wypisuje nic, jeśli nie ma takiego dopasowania (jeśli basename nie ma rozszerzenia lub katalog główny przed rozszerzeniem jest zbyt krótki). W zależności od wymagań możesz dostosować wyrażenie regularne. Wyrażenie regularne wymusza ograniczenia:

  1. Dopasowuje 3 znaki przed końcowym rozszerzeniem (część po ostatniej kropce i włącznie). Te 3 znaki mogą zawierać kropkę.
  2. Rozszerzenie może być puste (z wyjątkiem kropki).
  3. Dopasowana część i przedłużenie muszą być częścią basename (część po ostatnim ukośniku).

Używanie tego w podstawianiu poleceń ma normalne problemy z usuwaniem zbyt wielu końcowych znaków nowej linii, problem, który również wpływa na odpowiedź Stéphane'a. W obu przypadkach można sobie z tym poradzić, ale tutaj jest trochę łatwiej:

lastpart=$(
  perl -e 'print "$1x" if shift =~ m{ ( [^/]{3} ) [.] [^./]* \z }x' -- "$file"
)
lastpart=${lastpart%x}  # allow for possible trailing newline
jrw32982 obsługuje Monikę
źródło
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
źródło
0

Myślę, że ta funkcja bash, pathStr (), zrobi to, czego szukasz.

Nie wymaga awk, sed, grep, perl lub expr. Wykorzystuje tylko wbudowane bash, więc jest dość szybki.

Dołączyłem również zależne funkcje argsNumber i isOption, ale ich funkcje można łatwo włączyć do pathStr.

Funkcja zależna ifHelpShow nie jest uwzględniona, ponieważ ma wiele zależności zależnych do wyprowadzania tekstu pomocy albo w linii poleceń terminala, albo w oknie dialogowym GUI przez YAD . Przekazany do niego tekst pomocy jest dołączony do dokumentacji. Poradę, jeśli chcesz, jeśli ifHelpShow i jego podopieczni.

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
}

ZASOBY

DocSalvager
źródło
Nie rozumiem - zostało już tutaj pokazane, jak zrobić podobne w pełni przenośne - bez bashismów - pozornie prostsze niż to. Co to jest ${#@}?
mikeserv
To po prostu pakuje funkcjonalność w funkcję wielokrotnego użytku. re: $ {# @} ... Manipulowanie tablicami i ich elementami wymaga pełnej notacji zmiennej $ {}. $ @ to „tablica” argumentów. $ {# @} to składnia bash dla liczby argumentów.
DocSalvager
Nie, $#jest składnią liczby argumentów i jest ona również używana gdzie indziej.
mikeserv
Masz rację, że „$ #” jest szeroko udokumentowanym systax dla „liczby argumentów”. Jednak właśnie potwierdziłem, że „$ {# @}” jest równoważne. Skończyło się to po eksperymentowaniu z różnicami i podobieństwami między argumentami pozycyjnymi i tablicami. Później pochodzi ze składni tablicowej, która najwyraźniej jest synonimem krótszej, prostszej składni „$ #”. Zmieniłem i udokumentowałem argsNumber (), aby użyć „$ #”. Dzięki!
DocSalvager,
${#@}nie jest równoważny w większości przypadków - specyfikacja POSIX podaje wyniki dowolnych rozszerzeń parametrów na jednym z nich $@lub $*są niestety nieokreślone. Może to działać, bashale nie jest to niezawodna funkcja, myślę, że to, co próbuję powiedzieć.,
mikeserv