วิธีที่สั้นที่สุดในการแยก 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ฉันคาดเดา
Jason 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สำหรับหาง (basename) :rสำหรับส่วนที่เหลือ (ที่มีนามสกุลออก))


2
ดี exprเป็นอีกสิ่งหนึ่งที่ฉันต้องทำความคุ้นเคย ฉันจริงๆเช่นzshการแก้ปัญหาในทั่วไป (ผมก็แค่อ่านเกี่ยวกับการสนับสนุนสำหรับการแทนซ้อนกันที่ด้านซ้ายของ${}เมื่อวานนี้เกินไปและขออวยพรให้shมีเหมือนกัน) ก็แค่คนเกียจคร้านว่ามันไม่ได้เป็นปัจจุบันโดยค่าเริ่มต้นเสมอ
Jason C

2
@ JasonC - ข้อมูลสำคัญที่สุด จงทำให้ดีที่สุดเท่าที่จะทำได้เท่าที่จะทำได้ - นั่นคือจุดรวมของระบบ หากตัวแทนซื้ออาหารฉันอาจรู้สึกไม่พอใจ แต่บ่อยครั้งที่ข้อมูล(มากกว่าไม่เคย)นำกลับบ้านเบคอน
mikeserv

1
@mikeserv "คำขอ: แลกเปลี่ยนตัวแทนสำหรับเบคอน"; มองเมตาที่นี่ฉันมา
Jason C

1
@mikerserv ของคุณคือ POSIX ใช้ builtins เท่านั้นและไม่แยกกระบวนการใด ๆ การไม่ใช้การทดแทนคำสั่งหมายถึงคุณหลีกเลี่ยงปัญหาเกี่ยวกับการขึ้นบรรทัดใหม่ดังนั้นจึงเป็นคำตอบที่ดีเช่นกัน
Stéphane Chazelas

1
@mikeserv ผมไม่ได้หมายถึงการที่จะบ่งบอกexprได้รับไม่ POSIX มันเป็นอย่างแน่นอน แม้ว่าจะมีอยู่แล้วในตัว
Stéphane 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

การรวม$IFSกับsetพารามิเตอร์ ting shell สามารถเป็นวิธีการแยกวิเคราะห์และเจาะผ่านตัวแปรเชลล์ที่มีประสิทธิภาพมาก:

(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" แทนที่จะเป็น "file") ฉันเพิ่งเรียนรู้มากมายเกี่ยวกับการทดแทนพารามิเตอร์ ฉันไม่รู้ว่ามันสามารถทำได้ ... มีประโยชน์มาก ฉันคิดว่ามันอ่านง่ายมากเช่นกันเป็นการส่วนตัว
Jason C

1
@ JasonC - นี่เป็นพฤติกรรมแบบพกพาอย่างสมบูรณ์ - ไม่เฉพาะเจาะจง ผมขอแนะนำให้อ่านนี้
mikeserv

1
ดีฉันเดาฉันจะสามารถใช้%แทนการที่จะเอาคำต่อท้ายและฉันไม่จำเป็นต้องจริงจะตัดเส้นทางดังนั้นฉันจะได้รับดีกว่าสองบรรทัด%% noextn=${var%.*} ; lastpart=${noextn#${noextn%???}}
Jason 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")"
)

มันเจ๋งมาก. ได้รับการโหวต ny
mikeserv

กระชับขึ้นอีกหน่อย: perl -e 'shift =~ /(.{3})\.[^.]*$/ && print $1' $filename. เพิ่มเติมbasenameจะต้องถ้าชื่อไฟล์อาจมีไม่มีคำต่อท้าย แต่บางไดเรกทอรีในเส้นทางไม่
Dubu

@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 พร้อมใช้งานฉันพบว่าสามารถอ่านได้มากกว่าโซลูชันอื่น ๆ โดยเฉพาะเนื่องจากภาษา regex นั้นมีความชัดเจนและมี/xตัวแก้ไขซึ่งอนุญาตให้เขียน regex ที่ชัดเจนกว่า:

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

สิ่งนี้จะพิมพ์อะไรถ้าไม่มีการจับคู่ดังกล่าว (ถ้าชื่อฐานไม่มีส่วนขยายหรือถ้ารากก่อนส่วนขยายสั้นเกินไป) คุณสามารถปรับ regex ได้ทั้งนี้ขึ้นอยู่กับความต้องการของคุณ regex นี้บังคับใช้ข้อ จำกัด :

  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

ฉันคิดว่าฟังก์ชั่นทุบตีนี้ pathStr () จะทำสิ่งที่คุณกำลังมองหา

ไม่ต้องการ awk, sed, grep, perl หรือ expr ใช้ bash builtins เท่านั้นดังนั้นมันจึงค่อนข้างเร็ว

ฉันยังได้รวมฟังก์ชัน argsNumber และ isOption ที่พึ่งพาได้ด้วย

ifHelpShow ฟังก์ชั่นขึ้นอยู่ไม่ได้รวมในขณะที่มันมี subdependencies มากมายสำหรับการแสดงผลข้อความช่วยเหลือทั้งใน commandline ขั้วหรือกล่องโต้ตอบ GUI ผ่านYad ข้อความช่วยเหลือที่ส่งไปยังเอกสารนี้จะรวมอยู่ในเอกสาร ให้คำแนะนำหากคุณต้องการความช่วยเหลือหากแสดงและผู้ติดตาม

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

นี่เป็นเพียงการจัดแพคเกจการทำงานให้เป็นฟังก์ชันที่ใช้ซ้ำ เรื่อง: $ {# @} ... การจัดการอาร์เรย์และองค์ประกอบของพวกเขาต้องใช้สัญกรณ์ตัวแปรเต็ม $ {} $ @ คือ 'อาร์เรย์' ของอาร์กิวเมนต์ $ {# @} เป็นไวยากรณ์ bash สำหรับจำนวนอาร์กิวเมนต์
DocSalvager

ไม่$#เป็นไวยากรณ์สำหรับจำนวนอาร์กิวเมนต์และใช้ที่อื่นที่นี่
mikeserv

คุณพูดถูกว่า "$ #" เป็น systax ที่มีการบันทึกไว้อย่างกว้างขวางสำหรับ อย่างไรก็ตามฉันเพิ่งยืนยันว่า "$ {# @}" เทียบเท่ากัน ฉันกระทบกระทั่งกับสิ่งนั้นหลังจากทดลองกับความแตกต่างและความคล้ายคลึงกันระหว่างข้อโต้แย้งตำแหน่งและอาร์เรย์ ต่อมามาจากไวยากรณ์ของอาเรย์ซึ่งเห็นได้ชัดว่าเป็นคำพ้องสำหรับไวยากรณ์ที่สั้นลงและง่ายขึ้น "$ #" ฉันได้แก้ไขและบันทึก argsNumber () เพื่อใช้ "$ #" ขอบคุณ!
DocSalvager

${#@}ในกรณีส่วนใหญ่จะไม่เทียบเท่า - ข้อมูลจำเพาะ POSIX ระบุผลลัพธ์ของการขยายพารามิเตอร์ใด ๆ$@หรือ$*ไม่ระบุอย่างใดอย่างหนึ่งโชคไม่ดี อาจใช้งานได้bashแต่นั่นไม่ใช่คุณลักษณะที่เชื่อถือได้ฉันเดาว่าเป็นสิ่งที่ฉันพยายามจะพูด
mikeserv
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.