Cara terpendek untuk mengekstrak 3 karakter terakhir dari nama file base (minus suffix)


12

Saya mencoba untuk menetapkan variabel dalam skrip sh ke 3 karakter terakhir dari nama dasar file (dengan nama dasar yang saya maksud tanpa path dan tanpa akhiran). Saya telah berhasil melakukan ini, tetapi semata-mata karena penasaran, saya bertanya-tanya apakah ada perintah tunggal yang lebih pendek yang dapat saya gunakan. Awalnya saya punya satu kalimat awk, tapi itu agak panjang. Saat ini saya memiliki skrip dua baris ini (dengan asumsi nama file lengkap ada di $1):

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

Jadi misalnya, "/path/to/somefile.txt" berakhir dengan "ile" di $lastpart.

Dapatkah saya entah bagaimana menggabungkan basenamedan sedikit untuk menghapus sufiks menjadi satu perintah, dan apakah ada cara untuk mengirimnya tail(atau sesuatu yang bisa saya gunakan) tanpa menggunakan pipa? Akhiran tidak diketahui jadi saya tidak bisa mendasarkannya sebagai parameter basename.

Tujuan utama sebenarnya tidak sesingkat mungkin, untuk dibaca sekilas mungkin. Konteks sebenarnya dari semua ini adalah pertanyaan tentang Superuser ini , di mana saya mencoba memberikan jawaban yang cukup sederhana.


2
Bagaimana Anda menangani file seperti file.one.two.three? Apakah kamu mau ileatau two?
terdon

@terdon twoakan bekerja; ekstensi itu akan .threesaya kira.
Jason C

Jawaban:


6

Itu pekerjaan yang khas untuk expr:

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

Jika Anda tahu nama file Anda memiliki format yang diharapkan (berisi satu dan hanya satu titik dan setidaknya 3 karakter sebelum titik), itu dapat disederhanakan menjadi:

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

Perhatikan bahwa status keluar akan menjadi nol jika tidak ada kecocokan, tetapi juga jika bagian yang cocok adalah angka yang dipecahkan menjadi 0. (seperti untuk a000.txtatau a-00.txt)

Dengan zsh:

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

( :tuntuk ekor (nama belakang), :runtuk istirahat (dengan ekstensi dihapus)).


2
Bagus. expradalah satu lagi yang perlu saya kenal. Saya sangat suka zshsolusi secara umum (saya baru saja membaca tentang dukungannya untuk substitusi bersarang di sisi kiri ${}kemarin juga dan berharap shmemiliki hal yang sama), itu hanya mengecewakan bahwa itu tidak selalu hadir secara default.
Jason C

2
@JasonC - informasi yang paling penting. Manfaatkan sebaik mungkin agar dapat diakses - itulah inti dari sistem ini. Jika rep membeli makanan saya mungkin kesal, tetapi lebih sering (daripada tidak pernah) info membawa pulang daging
mikeserv

1
@mikeserv "Permintaan: Tukar tukar bacon"; lihat meta di sini aku datang.
Jason C

1
@ mikerserv, milik Anda adalah POSIX, hanya menggunakan bawaan dan tidak melakukan proses apa pun. Tidak menggunakan substitusi perintah juga berarti Anda menghindari masalah dengan mengikuti baris baru, jadi itu juga jawaban yang bagus.
Stéphane Chazelas

1
@ mikeserv, saya tidak bermaksud mengatakan expritu bukan POSIX. Tentu saja. Meskipun jarang built-in.
Stéphane Chazelas

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

###OUTPUT###

456

Yang pertama menghapus tiga karakter terakhir dari $varkemudian menghapus dari $varhasil penghapusan itu - yang mengembalikan tiga karakter terakhir $var. Berikut adalah beberapa contoh yang lebih khusus ditujukan untuk menunjukkan bagaimana Anda dapat melakukan hal seperti itu:

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

Anda tidak perlu menyebarkan semua ini melalui begitu banyak perintah. Anda dapat memadatkan ini:

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

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

Menggabungkan $IFSdengan setparameter shell juga bisa menjadi cara yang sangat efektif untuk penguraian dan pengeboran melalui variabel shell:

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

Yang akan membuat Anda hanya tiga karakter segera sebelum periode pertama setelah yang terakhir /di $path. Jika Anda ingin mengambil hanya tiga karakter pertama segera sebelum yang terakhir .dalam $path (misalnya, jika ada kemungkinan lebih dari satu .dalam nama file) :

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

Dalam kedua kasus yang dapat Anda lakukan:

newvar=$(IFS...)

Dan...

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

... akan mencetak yang mengikuti .

Jika Anda tidak keberatan menggunakan program eksternal yang dapat Anda lakukan:

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

Jika ada kemungkinan \nkarakter ewline dalam nama file (tidak berlaku untuk solusi shell asli - mereka semua mengatasinya) :

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

1
Ya, terima kasih. Saya juga menemukan dokumentasi . Tetapi untuk mendapatkan 3 karakter terakhir dari $basesana, yang terbaik yang bisa saya lakukan adalah tiga baris name=${var##*/} ; base=${name%%.*} ; lastpart=${base#${base%???}}. Di sisi positifnya itu adalah bash murni, tetapi masih 3 baris. (Dalam contoh Anda dari "/tmp/file.txt" Saya perlu "ile" daripada "file".) Saya baru saja belajar banyak tentang penggantian parameter; Saya tidak tahu itu bisa melakukan itu ... sangat berguna. Saya merasa itu sangat mudah dibaca, juga, secara pribadi.
Jason C

1
@JasonC - ini adalah perilaku sepenuhnya portabel - ini bukan spesifik bash. Saya sarankan membaca ini .
mikeserv

1
Yah, saya kira, saya bisa menggunakan %alih-alih %%menghapus sufiks, dan saya sebenarnya tidak perlu menghapus jalur, jadi saya bisa mendapatkan dua baris yang lebih bagus noextn=${var%.*} ; lastpart=${noextn#${noextn%???}}.
Jason C

1
@ JasonC - ya, sepertinya itu akan berhasil. Ini akan pecah jika ada $IFSdi ${noextn}dan Anda tidak mengutip ekspansi. Jadi, ini lebih aman:lastpart=${noextn#"${noextn%???}"}
mikeserv

1
@JasonC - terakhir, jika Anda menemukan di atas membantu, Anda mungkin ingin melihat ini . Ini berhubungan dengan bentuk lain dari ekspansi parameter dan jawaban lain untuk pertanyaan itu juga sangat bagus. Dan ada tautan ke dua jawaban lain pada subjek yang sama di dalamnya. Jika kamu mau.
mikeserv

4

Jika Anda dapat menggunakan perl:

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

itu keren. mendapat suara ny.
mikeserv

Sedikit lebih ringkas: perl -e 'shift =~ /(.{3})\.[^.]*$/ && print $1' $filename. Tambahan basenameakan diperlukan jika nama file mungkin tidak mengandung akhiran tetapi beberapa direktori di jalur tidak.
Dubu

@Dubu: Solusi Anda selalu gagal jika nama file tidak memiliki akhiran.
cuonglm

1
@ Gnouc Ini sengaja. Tapi Anda benar, ini bisa salah tergantung tujuannya. Alternatif:perl -e 'shift =~ m#(.{3})(?:\.[^./]*)?$# && print $1' $filename
Dubu

2

sed bekerja untuk ini:

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

Atau

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

Jika Anda sedtidak mendukung -r, ganti saja instance ()dengan \(dan \), dan kemudian -rtidak diperlukan.


1

Jika perl tersedia, saya merasa ini bisa lebih mudah dibaca daripada solusi lain, khususnya karena bahasa /xregexnya lebih ekspresif dan memiliki modifikator, yang memungkinkan untuk menulis regex yang lebih jelas:

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

Ini tidak mencetak apa pun jika tidak ada yang cocok (jika nama dasarnya tidak memiliki ekstensi atau jika root sebelum ekstensi terlalu pendek). Tergantung pada kebutuhan Anda, Anda dapat menyesuaikan regex. Regex ini memberlakukan batasan:

  1. Ini cocok dengan 3 karakter sebelum ekstensi akhir (bagian setelah dan termasuk titik terakhir). 3 karakter ini dapat berisi titik.
  2. Ekstensi dapat kosong (kecuali untuk titik).
  3. Bagian yang cocok dan ekstensi harus menjadi bagian dari nama dasar (bagian setelah garis miring terakhir).

Menggunakan ini dalam substitusi perintah memiliki masalah normal dengan menghapus terlalu banyak baris baru, masalah yang juga mempengaruhi jawaban Stéphane. Ini dapat ditangani dalam kedua kasus, tetapi sedikit lebih mudah di sini:

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

Saya pikir fungsi bash ini, pathStr (), akan melakukan apa yang Anda cari.

Itu tidak membutuhkan awk, sed, grep, perl atau expr. Hanya menggunakan bash builtins sehingga cukup cepat.

Saya juga menyertakan fungsi argsNumber dan isOption yang tergantung, tetapi fungsinya dapat dengan mudah dimasukkan ke pathStr.

Fungsi dependen ifHelpShow tidak termasuk karena memiliki banyak subdependensi untuk menghasilkan teks bantuan baik pada commandline terminal atau ke kotak dialog GUI via YAD . Teks bantuan yang diteruskan disertakan untuk dokumentasi. Anjurkan jika Anda ingin ifHelpShow dan tanggungannya.

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
}

SUMBER DAYA


Saya tidak mengerti - ini sudah diperagakan di sini bagaimana melakukan hal yang sama sepenuhnya mudah dibawa - tanpa bashisme - tampaknya lebih sederhana dari ini. Apa itu ${#@}?
mikeserv

Ini hanya mengemas fungsionalitas menjadi fungsi yang dapat digunakan kembali. re: $ {# @} ... Memanipulasi array dan elemen-elemennya memerlukan notasi variabel penuh $ {}. $ @ adalah 'array' argumen. $ {# @} adalah sintaks bash untuk jumlah argumen.
DocSalvager

Tidak, $#adalah sintaks untuk jumlah argumen, dan juga digunakan di tempat lain di sini.
mikeserv

Anda benar bahwa "$ #" adalah systax yang banyak didokumentasikan untuk "jumlah argumen." Namun, saya baru saja memverifikasi bahwa "$ {# @}" sama. Saya akhirnya dengan itu setelah bereksperimen dengan perbedaan dan persamaan antara argumen posisi dan array. Nanti berasal dari sintaks array yang ternyata adalah sinonim untuk sintaksis "$ #" yang lebih pendek dan lebih sederhana. Saya telah mengubah dan mendokumentasikan argsNumber () untuk menggunakan "$ #". Terima kasih!
DocSalvager

${#@}dalam banyak kasus tidak setara - spec POSIX menyatakan hasil dari setiap ekspansi parameter pada salah satu $@atau $*tidak ditentukan, sayangnya. Mungkin berfungsi, bashtetapi itu bukan fitur yang dapat diandalkan, saya kira itulah yang ingin saya katakan.,
mikeserv
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.