Кратчайший способ извлечь последние 3 символа базового (без суффикса) имени файла

12

Я пытаюсь установить переменную в скрипте sh для последних 3 символов базового имени файла (под базовым именем я имею в виду без пути и без суффикса). Я преуспел в этом, но, просто из любопытства, мне интересно, есть ли более короткая, единственная команда, которую я могу использовать. Первоначально у меня был однострочник awk, но это было довольно долго. В настоящее время у меня есть этот двухстрочный скрипт (при условии, что полное имя файла находится в $1):

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

Так, например, «/path/to/somefile.txt» заканчивается на «ile» в $lastpart.

Можно ли как-то объединить basenameи бит, чтобы разделить суффикс в одну команду, и есть ли способ отправить его tail(или что-то еще, что я могу использовать) без использования канала? Суффикс неизвестен, поэтому я не могу использовать его в качестве параметра basename.

На самом деле главная цель не столько в том, чтобы быть как можно короче, сколько в том, чтобы быть читабельным с первого взгляда. Фактический контекст всего этого - этот вопрос на Superuser , где я пытаюсь найти достаточно простой ответ.

Джейсон С
источник
2
Как бы вы справились с файлом, как file.one.two.three? Хотели бы вы ileили two?
Тердон
@terdon twoбудет работать; расширение на это было бы, .threeя думаю.
Джейсон С,

Ответы:

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было то же самое), это просто облом, который не всегда присутствует по умолчанию.
Джейсон С
2
@JasonC - информация важнее всего. Сделать все возможное максимально доступным - в этом и заключается весь смысл системы. Если представитель купил еду, я мог бы расстроиться, но чаще (чем никогда) информация приносит домой бекон
mikeserv
1
@mikeserv "Запрос: обменять репутацию на бекон"; смотри мета здесь я приду.
Джейсон С,
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/'
mikeserv
источник
1
Это спасибо. Я также нашел документацию . Но чтобы получить $baseоттуда последние 3 символа , лучшее, что я мог сделать, - это три строки name=${var##*/} ; base=${name%%.*} ; lastpart=${base#${base%???}}. С положительной стороны это чистый bash, но он по-прежнему 3 строки. (В вашем примере «/tmp/file.txt» мне понадобится «ile», а не «file».) Я только что многому научился о замене параметров; Я понятия не имел, что это могло бы сделать это ... довольно удобно. Лично я нахожу это очень читабельным.
Джейсон C
1
@JasonC - это полностью переносимое поведение - оно не относится к bash. Я рекомендую прочитать это .
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")"
)
cuonglm
источник
это круто. получил голосование.
mikeserv
Немного более кратким: perl -e 'shift =~ /(.{3})\.[^.]*$/ && print $1' $filename. Дополнительное basenameбудет необходимо, если имя файла может не содержать суффикса, но какой-то каталог в пути содержит.
Дабу
@Dubu: Ваше решение всегда терпит неудачу, если имя файла не имеет суффикса.
Cuonglm
1
@Gnouc Это было намеренно. Но вы правы, это может быть неправильно в зависимости от цели. Альтернатива:perl -e 'shift =~ m#(.{3})(?:\.[^./]*)?$# && print $1' $filename
Дабу
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не требуется.

BenjiWiebe
источник
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
jrw32982 поддерживает Монику
источник
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
источник
0

Я думаю, что эта функция bash, pathStr (), сделает то, что вы ищете.

Не требует awk, sed, grep, perl или expr. Он использует только встроенные команды Bash, поэтому он довольно быстрый.

Я также включил зависимые функции argsNumber и isOption, но их функции могут быть легко включены в pathStr.

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

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
}

РЕСУРСЫ

DocSalvager
источник
Я не понимаю - здесь уже было продемонстрировано, как сделать подобное полностью переносимым - без bashisms - на вид проще, чем это. Кроме того, что это ${#@}?
mikeserv
Это просто упаковывает функциональность в функцию многократного использования. re: $ {# @} ... Для управления массивами и их элементами требуется полная переменная запись $ {}. $ @ - это «массив» аргументов. $ {# @} - синтаксис bash для количества аргументов.
DocSalvager
Нет, $#это синтаксис для количества аргументов, и он также используется в другом месте здесь.
mikeserv
Вы правы, что «$ #» - это широко документированный систакс для «количества аргументов». Однако, я только что подтвердил, что "$ {# @}" эквивалентно. Я закончил с этим после экспериментов с различиями и сходствами между позиционными аргументами и массивами. Последнее происходит от синтаксиса массива, который, очевидно, является синонимом для более короткого и простого синтаксиса «$ #». Я изменил и задокументировал argsNumber () для использования "$ #". Благодарность!
DocSalvager
${#@}в большинстве случаев не эквивалентен - в спецификации POSIX указаны результаты любых расширений параметров либо, $@либо $*, к сожалению, они не определены. Это может работать, bashно это не надежная функция, я думаю, это то, что я пытаюсь сказать.,
mikeserv