提取基本(减后缀)文件名的最后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

@terdon two可以工作;.three我想这是扩展。
杰森C

Answers:


6

这是典型的工作expr

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

如果您知道文件名具有预期的格式(仅包含一个点,且点之前至少包含三个字符),则可以将其简化为:

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

请注意,如果没有匹配项,但如果匹配的部分是一个解析为0的数字,则退出状态将为非零。(例如for a000.txta-00.txt

zsh

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

:t用于尾部(基本名称),:r用于其余(扩展名已删除))。


2
真好 expr是我需要熟悉的另一个。我真的很喜欢这些zsh解决方案(我只是在${}昨天也阅读了它对嵌套替换的支持,并希望sh有同样的解决方案),这实在令人遗憾,默认情况下并不总是存在。
詹森C

2
@JasonC-信息最重要。尽可能充分地利用它-无论如何这就是系统的重点。如果销售代表买了食物,我可能会感到不高兴,但更多(而不是从来没有)信息将培根带回家
mikeserv 2014年

1
@mikeserv“请求:将培根换成代表”;看看元,我来了。
詹森·C

1
@mikerserv,您的是POSIX,仅使用内置函数,不会派生任何进程。不使用命令替换还意味着您可以避免在换行符结尾处出现问题,因此这也是一个很好的答案。
斯特凡Chazelas

1
@mikeserv,我不是故意暗示expr不是 POSIX。那当然是。它很少是内置的。
斯特凡Chazelas

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

结合$IFSset婷壳参数也可以解析和通过外壳变量钻的一个非常有效的手段:

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

这样一来,您只会获得紧接最后一个/in之后第一个句点之前的三个字符$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文件名中有可能出现斜线字符(不适用于本机外壳解决方案-无论如何,它们都可以处理)

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

1
是的,谢谢。我还找到了文档。但是要从$base那里获得最后3个字符,我能做的最好的就是三行name=${var##*/} ; base=${name%%.*} ; lastpart=${base#${base%???}}。从好的方面来说,它是纯bash,但仍然是3行。(在您的“ /tmp/file.txt”示例中,我需要“ ile”而不是“ file”。)我确实学到了很多关于参数替换的知识;我不知道它能做到...非常方便。我个人也确实很可读。
詹森C

1
@JasonC-这是完全可移植的行为-它不特定于bash。我建议读这篇
mikeserv 2014年

1
好吧,我想,我可以使用%而不是%%删除后缀,并且我实际上不需要剥离路径,因此我可以获得一条更好的两行代码noextn=${var%.*} ; lastpart=${noextn#${noextn%???}}
2014年

1
@JasonC-是的,看起来好像可以。如果有,它将中断$IFS${noextn}并且您不引用扩展。因此,这更安全:lastpart=${noextn#"${noextn%???}"}
mikeserv

1
@JasonC -最后,如果你发现上面有帮助的,你可能想看看这个。它处理其他形式的参数扩展,并且对该问题的其他答案也确实很好。并且其中有指向同一主题的其他两个答案的链接。如果你想。
mikeserv 2014年

4

如果你可以使用perl

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

太棒了。得到纽约投票。
mikeserv

有点更简洁:perl -e 'shift =~ /(.{3})\.[^.]*$/ && print $1' $filenamebasename如果文件名不包含后缀,但路径中的某些目录包含后缀,则将需要一个附加文件。
Dubu 2014年

@Dubu:如果文件名没有后缀,您的解决方案将始终失败。
cuonglm

1
@Gnouc这是出于故意。但是您是对的,根据目的,这可能是错误的。另类:perl -e 'shift =~ m#(.{3})(?:\.[^./]*)?$# && print $1' $filename
Dubu 2014年

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,我会发现它比其他解决方案更具可读性,特别是因为它的regex语言更具表现力,并且具有/x修饰符,从而可以编写更清晰的regex:

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

如果不存在这样的匹配项(如果基名没有扩展名,或者扩展名之前的根太短),则此命令不打印任何内容。根据您的要求,您可以调整正则表达式。此正则表达式强制执行约束:

  1. 它与最后一个扩展名(最后一个点之后并包括最后一个点)的3个字符匹配。这3个字符可以包含一个点。
  2. 该扩展名可以为空(点除外)。
  3. 匹配的部分和扩展名必须是基本名称的一部分(最后一个斜杠之后的部分)。

在命令替换中使用此命令通常会导致删除过多的尾随换行符,这也影响了Stéphane的答案。两种情况都可以处理,但是在这里要容易一些:

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,因为它具有许多子依赖关系,这些子依赖关系可在终端命令行或通过YAD向GUI对话框输出帮助文本。传递给它的帮助文本包含在文档中。建议您是否需要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
}

资源


我不明白-在这里已经演示了如何完全可移植地进行类似操作-没有bashisms-似乎比这更简单。另外,什么是${#@}
mikeserv

这只是将功能打包成可重用的功能。re:$ {#@} ...操作数组及其元素需要完整的变量符号$ {}。$ @是参数的“数组”。$ {#@}是参数数量的bash语法。
DocSalvager 2014年

不,$#这是参数数量的语法,在这里其他地方也使用它。
mikeserv

没错,“ $#”是广泛记载的“参数数量”的系统税。但是,我刚刚重新验证了“ $ {#@}”是否等效。在尝试过位置参数和数组之间的差异和相似性之后,我总结了这一点。后者来自数组语法,显然是较短,更简单的“ $#”语法的同义词。我已经更改并记录了argsNumber()以使用“ $#”。谢谢!
DocSalvager 2014年

${#@}在大多数情况下不是等效的- 不幸的是,POSIX规范指出了任一参数扩展$@或未$*指定的参数扩展结果。它可能会起作用,bash但这不是一个可靠的功能,我想这就是我想说的。,
mikeserv 2014年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.