Найкоротший спосіб вилучення останніх 3 символів базового (мінус суфікса) імені файлу


12

Я намагаюся встановити змінну в скрипті sh для останніх 3 символів базового імені файлу (під ім'ям бази я маю на увазі без шляху та без суфікса). Мені це вдалося зробити, але, з цікавості, мені цікаво, чи є коротша, єдина команда, яку я можу використовувати. Спочатку у мене був одноводковий awk, але він був досить довгим. На даний момент у мене є цей дворядковий сценарій (якщо повна назва файлу є $1):

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

Так, наприклад, "/path/to/somefile.txt" закінчується "ile" в $lastpart.

Чи можу я якось комбінувати basenameі біт, щоб зняти суфікс в одну команду, і чи є спосіб надіслати його tail(або щось інше, що я можу використовувати), не використовуючи трубу? Суфікс невідомий, тому я не можу базувати його як параметр basename.

Основна мета насправді не стільки бути максимально короткою, скільки бути читабельною на перший погляд. Справжнім контекстом усього цього є це питання про Суперрусера , де я намагаюся знайти досить просту відповідь.


2
Як би ти поводився з таким файлом file.one.two.three? Хочете ileчи two?
тердон

@terdon працював twoби; продовження на це було б .threeя здогадуюсь.
Джейсон C

Відповіді:


6

Це типова робота для expr:

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

Якщо ви знаєте, що ваші імена файлів мають очікуваний формат (містить одну і лише одну крапку і щонайменше 3 символи перед крапкою), це можна спростити до:

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

Зауважте, що статус виходу буде не нульовим, якщо немає відповідності, але також якщо відповідна частина - це число, яке дорівнює 0. (наприклад, для a000.txtабо a-00.txt)

З zsh:

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

( :tдля хвоста (основна назва), :rдля відпочинку (з розширенням видалено)).


2
Приємно. exprце ще один, з яким мені потрібно ознайомитися. Мені дуже подобаються zshрішення в цілому (я просто читав про його підтримку вкладених підстановок ліворуч від ${}вчорашнього дня і, бажаючи, щоб це shбуло), це просто облом, який не завжди присутній за замовчуванням.
Джейсон C

2
@JasonC - найважливіша інформація. Зробіть все можливе максимально доступним - ось і вся суть системи. Якщо реп купив їжу, я можу засмутитися, але частіше (ніж ніколи) інформація приносить додому бекон
mikeserv

1
@mikeserv "Запит: обмін представниками на бекон"; дивись мета тут я приходжу.
Джейсон C

1
@mikerserv, ваш - POSIX, використовує лише вбудовані файли і не розгортає жодного процесу. Якщо не використовувати підстановку команд, це також означає, що ви уникнете проблем із затримкою нових рядків, тому це також хороша відповідь.
Стефан Шазелас

1
@mikeserv, я не мав на увазі, що exprце не POSIX. Це, безумовно, є. Це рідко вбудований, хоча.
Стефан Шазелас

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

###OUTPUT###

456

Це спочатку видаляє три останні символи, $varа потім видаляє $varрезультати цього видалення - що повертає останні три символи $var. Ось кілька прикладів, більш конкретно спрямованих на демонстрацію того, як ви можете зробити таке:

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

Вам не доведеться поширювати все це через стільки команд. Ви можете компактне:

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

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

Поєднання $IFSз setпараметрами оболонки ting також може бути дуже ефективним засобом розбору та свердління за допомогою змінних оболонок:

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

Це дозволить отримати лише три символи, що передують першому періоду після останнього /в $path. Якщо ви хочете отримати лише перші три символи, що передують останньому .в $path (наприклад, якщо .в імені файлу є можливість більше одного ) :

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

В обох випадках ви можете:

newvar=$(IFS...)

І ...

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

... надрукує те, що слідує за цим .

Якщо ви не проти використовувати зовнішню програму, ви можете:

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

Якщо є \nім'я символу ewline у ​​назві файлу (не застосовується для нативних рішень оболонки - вони все одно справляються з цим) :

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

1
Це, дякую. Я також знайшов документацію . Але щоб отримати останніх 3 символи $baseзвідти, найкраще, що я міг зробити, це трирядковий name=${var##*/} ; base=${name%%.*} ; lastpart=${base#${base%???}}. З плюсу - це чистий баш, але це все-таки 3 лінії. (У вашому прикладі "/tmp/file.txt" мені знадобиться "ile", а не "файл".) Я просто дізнався багато про підміну параметрів; Я не мав ідеї, що це може зробити ... досить зручно. Я вважаю це дуже читабельним, також особисто.
Джейсон C

1
@JasonC - це повністю портативна поведінка - це не конкретно. Я рекомендую прочитати це .
mikeserv

1
Ну, я думаю, я можу використовувати %замість того, %%щоб видаляти суфікс, і мені насправді не потрібно знімати шлях, тому я можу отримати приємніший, дворядковий noextn=${var%.*} ; lastpart=${noextn#${noextn%???}}.
Джейсон C

1
@JasonC - так, схоже, це спрацювало б. Він зламається , якщо є $IFSв ${noextn}і ви не процитувати розширення. Отже, це безпечніше:lastpart=${noextn#"${noextn%???}"}
mikeserv

1
@JasonC - останнє, якщо ви знайшли вищезгадане корисним, ви можете поглянути на це . Він стосується інших форм розширення параметрів, і інші відповіді на це питання теж дуже хороші. І є посилання на дві інші відповіді на ту саму тему. Якщо хочете.
mikeserv

4

Якщо ви можете використовувати perl:

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

це круто. отримав голос.
mikeserv

Трохи більш коротким: perl -e 'shift =~ /(.{3})\.[^.]*$/ && print $1' $filename. Додатковий basenameпотрібен, якщо ім'я файлу може містити не суфікс, але деякий каталог у шляху.
Дубу

@Dubu: Ваше рішення завжди виходить з ладу, якщо у файлі немає суфіксу.
cuonglm

1
@Gnouc Це було з наміром. Але ти маєш рацію, це може бути неправильним залежно від мети. Альтернатива:perl -e 'shift =~ m#(.{3})(?:\.[^./]*)?$# && print $1' $filename
Dubu

2

sed працює для цього:

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

Або

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

Якщо ваш sedне підтримує -r, просто замінити екземпляри ()з \(і \), а потім -rне потрібно.


1

Якщо perl доступний, я вважаю, що він може бути більш читабельним, ніж інші рішення, зокрема, тому, що мова його регулярних виразів є більш виразною та має /xмодифікатор, який дозволяє писати чіткіші регулярні вирази:

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

Це не надрукує нічого, якщо такого збігу немає (якщо базове ім’я не має розширення або якщо корінь перед розширенням занадто короткий). Залежно від ваших вимог, ви можете скорегувати регулярний вираз. Цей регулярний вимір виконує обмеження:

  1. Він відповідає 3 символам перед остаточним розширенням (частина після і включаючи останню крапку). Ці 3 символи можуть містити крапку.
  2. Розширення може бути порожнім (крім крапки).
  3. Зібрана частина та розширення повинні бути частиною базової назви (частини після останньої косої риски).

Використання цього в підстановці команди має звичайні проблеми із видаленням занадто багатьох останніх рядків, проблема, яка також впливає на відповідь Стефана. Це можна вирішити в обох випадках, але тут трохи простіше:

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

Я думаю, що ця функція bash, pathStr (), буде робити те, що ви шукаєте.

Тут не потрібно awk, sed, grep, perl або expr. Він використовує тільки bash вбудовані, так що це досить швидко.

Я також включив функції argsNumber та isOption, але їхні функції можна легко включити в pathStr.

Залежна функція ifHelpShow не включена, оскільки вона має численні підзалежності для виведення довідкового тексту або в командному рядку терміналу, або в діалоговому вікні GUI через YAD . Текст довідки, переданий йому, міститься для документації. Порадьте, якщо ви хочете, якщо helShow та його залежності.

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
}

РЕСУРСИ


Я не розумію - тут вже було зроблено уявлення про те, як зробити подібне повністю портативно - без bashізмів - начебто простіше, ніж це. Також, що таке ${#@}?
mikeserv

Це просто пакує функціональність у багаторазову функцію. re: $ {# @} ... Для маніпулювання масивами та їх елементами потрібна повна позначення змінної $ {}. $ @ - це масив аргументів. $ {# @} - синтаксис bash для кількості аргументів.
DocSalvager

Ні, $#це синтаксис кількості аргументів, і він також використовується в іншому місці.
mikeserv

Ви маєте рацію, що "$ #" - це широкодокументована система "кількості аргументів". Однак я щойно підтвердив, що "$ {# @}" еквівалентний. Я закінчив це після експерименту з відмінностями та подібністю між позиційними аргументами та масивами. Пізніше походить із синтаксису масиву, який, очевидно, є синонімом коротшого, простішого "$ #" синтаксису. Я змінив і задокументував argsNumber () для використання "$ #". Дякую!
DocSalvager

${#@}в більшості випадків не є еквівалентним - специфіка POSIX констатує результати будь-якого розширення параметрів $@або $*, на жаль, не визначені. Це може працювати, bashале це не є надійною особливістю, я думаю, це те, що я намагаюся сказати.,
mikeserv
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.