Cách ngắn nhất để trích xuất 3 ký tự cuối cùng của tên tệp cơ sở (trừ hậu tố)


12

Tôi đang cố gắng đặt một biến trong tập lệnh sh thành 3 ký tự cuối cùng của tên cơ sở của tệp (theo tên cơ sở tôi có nghĩa là không có đường dẫn không có hậu tố). Tôi đã thành công trong việc này, nhưng vì tò mò, tôi tự hỏi liệu tôi có thể sử dụng một lệnh ngắn hơn, ngắn hơn không. Ban đầu tôi có một lớp lót với awk, nhưng nó khá dài. Hiện tại tôi có tập lệnh hai dòng này (giả sử tên tệp hoàn chỉnh nằm trong $1):

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

Vì vậy, ví dụ, "/path/to/somefile.txt" đầu ra "ile" trong $lastpart.

Tôi có thể bằng cách nào đó kết hợp basenamevà bit để tách hậu tố thành một lệnh duy nhất không, và có cách nào để gửi nó đến tail(hoặc một cái gì khác tôi có thể sử dụng) mà không cần sử dụng đường ống không? Hậu tố không rõ vì vậy tôi không thể đặt nó làm tham số basename.

Mục tiêu chính thực sự không phải là quá ngắn để có thể càng ngắn càng tốt, càng nhanh càng dễ đọc. Bối cảnh thực tế của tất cả những điều này là câu hỏi này trên Superuser , nơi tôi đang cố gắng đưa ra một câu trả lời khá đơn giản.


2
Làm thế nào bạn sẽ đối phó với một tập tin như thế file.one.two.threenào? Bạn có muốn ilehay twokhông?
terdon

@terdon twosẽ làm việc; phần mở rộng trên đó sẽ là .threetôi đoán.
Jason C

Câu trả lời:


6

Đó là một công việc điển hình cho expr :

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

Nếu bạn biết tên tệp của mình có định dạng mong đợi (chứa một và chỉ một dấu chấm và ít nhất 3 ký tự trước dấu chấm), điều đó có thể được đơn giản hóa thành:

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

Lưu ý rằng trạng thái thoát sẽ khác không nếu không có kết quả khớp, nhưng cũng có thể nếu phần khớp là một số giải quyết thành 0. (như cho a000.txthoặc a-00.txt)

Với zsh:

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

( :tcho đuôi (tên cơ sở), :rcho phần còn lại (đã loại bỏ phần mở rộng)).


2
Đẹp. exprlà một cái khác tôi cần làm quen. Tôi thực sự thích các zshgiải pháp nói chung (tôi chỉ đọc về sự hỗ trợ của nó cho các thay thế lồng nhau ở bên trái của ${}ngày hôm qua và mong muốn shcũng vậy), đó chỉ là một mánh khóe mà nó không luôn luôn xuất hiện theo mặc định.
Jason C

2
@JasonC - thông tin quan trọng nhất. Tận dụng tốt nhất có thể truy cập như bạn có thể - đó là toàn bộ điểm của hệ thống. Nếu đại diện mua thực phẩm tôi có thể bực mình, nhưng thông tin thường xuyên hơn (không bao giờ) mang về nhà thịt xông khói
mikeerv

1
@mikeerv "Yêu cầu: Trao đổi đại diện cho thịt xông khói"; Nhìn ra meta ở đây tôi đến.
Jason C

1
@mikerserv, của bạn là POSIX, chỉ sử dụng nội trang và không rẽ nhánh bất kỳ quy trình nào. Không sử dụng thay thế lệnh cũng có nghĩa là bạn tránh được các vấn đề với dòng mới, vì vậy đây cũng là một câu trả lời hay.
Stéphane Chazelas

1
@mikeerv, tôi không có ý ám exprchỉ không phải là POSIX. Nó chắc chắn là như vậy. Nó hiếm khi được tích hợp sẵn.
Stéphane Chazelas

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

###OUTPUT###

456

Điều đó đầu tiên loại bỏ ba ký tự cuối cùng $varsau đó xóa khỏi $varkết quả của việc loại bỏ đó - trả về ba ký tự cuối cùng của $var. Dưới đây là một số ví dụ cụ thể hơn nhằm mục đích chứng minh làm thế nào bạn có thể làm một việc như vậy:

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

Bạn không cần phải truyền bá tất cả những điều này qua rất nhiều lệnh. Bạn có thể nén cái này:

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

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

Kết hợp $IFSvới setcác tham số shell ting cũng có thể là một phương tiện phân tích cú pháp và khoan thông qua các biến shell rất hiệu quả:

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

Điều đó sẽ giúp bạn có được chỉ có ba nhân vật ngay trước giai đoạn đầu sau khi cuối cùng /trong $path. Nếu bạn muốn lấy chỉ có ba chữ cái đầu tiên ngay trước cuối cùng .trong $path (ví dụ, nếu có một khả năng nhiều hơn một .trong filename) :

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

Trong cả hai trường hợp, bạn có thể làm:

newvar=$(IFS...)

Và ...

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

... sẽ in những gì sau .

Nếu bạn không phiền khi sử dụng chương trình bên ngoài, bạn có thể làm:

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

Nếu có một \nký tự ewline trong tên tệp (không áp dụng cho các giải pháp shell gốc - tất cả chúng đều xử lý điều đó) :

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

1
Đó là, cảm ơn. Tôi cũng đã tìm thấy tài liệu . Nhưng để có được 3 nhân vật cuối cùng từ $baseđó, điều tốt nhất tôi có thể làm là ba dòng name=${var##*/} ; base=${name%%.*} ; lastpart=${base#${base%???}}. Về mặt tích cực, đó là bash thuần túy, nhưng vẫn là 3 dòng. (Trong ví dụ của bạn về "/tmp/file.txt" Tôi cần "ile" thay vì "tập tin".) Tôi đã học được rất nhiều về việc thay thế tham số; Tôi không biết nó có thể làm điều đó ... khá tiện dụng. Cá nhân tôi cũng thấy nó rất dễ đọc.
Jason C

1
@JasonC - đây là hành vi hoàn toàn di động - nó không phải là bash cụ thể. Tôi khuyên bạn nên đọc .
mikeerv

1
Chà, tôi đoán, tôi có thể sử dụng %thay vì %%xóa hậu tố, và tôi thực sự không cần phải xóa đường dẫn, vì vậy tôi có thể có được một dòng hai đẹp hơn noextn=${var%.*} ; lastpart=${noextn#${noextn%???}}.
Jason C

1
@JasonC - vâng, có vẻ như nó sẽ hoạt động. Nó sẽ phá vỡ nếu có $IFStrong ${noextn}và bạn không trích dẫn mở rộng. Vì vậy, điều này an toàn hơn:lastpart=${noextn#"${noextn%???}"}
mikeerv

1
@JasonC - cuối cùng, nếu bạn thấy hữu ích ở trên, bạn có thể muốn xem xét điều này . Nó liên quan đến các hình thức mở rộng tham số khác và các câu trả lời khác cho câu hỏi đó cũng thực sự tốt. Và có liên kết đến hai câu trả lời khác về cùng một chủ đề trong. Nếu bạn muốn.
mikeerv

4

Nếu bạn có thể sử dụng perl:

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

thật là tuyệt. được ny bình chọn.
mikeerv

Một chút súc tích hơn : perl -e 'shift =~ /(.{3})\.[^.]*$/ && print $1' $filename. Một bổ sung basenamesẽ là cần thiết nếu tên tệp có thể không có hậu tố nhưng một số thư mục trong đường dẫn thì không.
Dubu

@Dubu: Giải pháp của bạn luôn thất bại nếu tên tệp không có hậu tố.
cuonglm

1
@Gnouc Đây là do chủ ý. Nhưng bạn đã đúng, điều này có thể sai tùy thuộc vào mục đích. Thay thế:perl -e 'shift =~ m#(.{3})(?:\.[^./]*)?$# && print $1' $filename
Dubu

2

sed làm việc cho điều này:

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

Hoặc là

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

Nếu bạn sedkhông hỗ trợ -r, chỉ cần thay thế các trường hợp ()với \(\), và sau đó -rlà không cần thiết.


1

Nếu perl có sẵn, tôi thấy nó có thể dễ đọc hơn các giải pháp khác, đặc biệt vì ngôn ngữ regex của nó có tính biểu cảm hơn và nó có công cụ /xsửa đổi, cho phép viết regex rõ ràng hơn:

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

Điều này in không có gì nếu không có kết quả khớp như vậy (nếu tên cơ sở không có phần mở rộng hoặc nếu gốc trước phần mở rộng quá ngắn). Tùy thuộc vào yêu cầu của bạn, bạn có thể điều chỉnh regex. Regex này thi hành các ràng buộc:

  1. Nó khớp với 3 ký tự trước phần mở rộng cuối cùng (phần sau và bao gồm dấu chấm cuối cùng). 3 ký tự này có thể chứa một dấu chấm.
  2. Phần mở rộng có thể để trống (ngoại trừ dấu chấm).
  3. Phần phù hợp và phần mở rộng phải là một phần của tên cơ sở (phần sau dấu gạch chéo cuối cùng).

Sử dụng điều này trong thay thế lệnh có các vấn đề bình thường với việc loại bỏ quá nhiều dòng mới, một vấn đề cũng ảnh hưởng đến câu trả lời của Stéphane. Nó có thể được xử lý trong cả hai trường hợp, nhưng ở đây dễ dàng hơn một chút:

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

Tôi nghĩ hàm bash này, pathStr (), sẽ làm những gì bạn đang tìm kiếm.

Nó không yêu cầu awk, sed, grep, perl hoặc expr. Nó chỉ sử dụng các nội dung bash nên khá nhanh.

Tôi cũng đã bao gồm các hàm argsNumber và isOption phụ thuộc nhưng các chức năng của chúng có thể dễ dàng được tích hợp vào pathStr.

Hàm phụ thuộc ifHelpShow không được bao gồm vì nó có nhiều phụ thuộc để xuất văn bản trợ giúp trên dòng lệnh đầu cuối hoặc vào hộp thoại GUI qua YAD . Các văn bản trợ giúp được chuyển đến nó được bao gồm cho tài liệu. Tư vấn nếu bạn muốn ifHelpShow và những người phụ thuộc của nó.

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
}

TÀI NGUYÊN


Tôi không hiểu - ở đây đã được giới thiệu cách thực hiện tương tự hoàn toàn có thể di chuyển - không có bashisms - có vẻ đơn giản hơn thế này. Ngoài ra, là ${#@}gì?
mikeerv

Điều này chỉ gói các chức năng thành một chức năng có thể tái sử dụng. re: $ {# @} ... Thao tác với các mảng và các phần tử của chúng yêu cầu ký hiệu biến đầy đủ $ {}. $ @ là 'mảng' của các đối số. $ {# @} là cú pháp bash cho số lượng đối số.
DocSalvager

Không, $#là cú pháp cho số lượng đối số và nó cũng được sử dụng ở những nơi khác ở đây.
mikeerv

Bạn đã đúng rằng "$ #" là systax được ghi chép rộng rãi cho "số lượng đối số". Tuy nhiên, tôi vừa hoàn nguyên rằng "$ {# @}" là tương đương. Tôi giải quyết vấn đề đó sau khi thử nghiệm sự khác biệt và tương đồng giữa các đối số vị trí và mảng. Cái sau xuất phát từ cú pháp mảng rõ ràng là một từ đồng nghĩa với cú pháp "$ #" ngắn hơn, đơn giản hơn. Tôi đã thay đổi và ghi lại argsNumber () để sử dụng "$ #". Cảm ơn!
DocSalvager

${#@}không tương đương trong hầu hết các trường hợp - thông số POSIX cho biết kết quả của bất kỳ mở rộng tham số nào trên một $@hoặc $*không xác định, không may. Nó có thể hoạt động bashnhưng đó không phải là một tính năng đáng tin cậy, tôi đoán là những gì tôi đang cố gắng nói.,
mikeerv
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.