Cách nhận đối số cuối cùng cho hàm / bin / sh


11

Cách tốt hơn để thực hiện là print_last_arggì?

#!/bin/sh

print_last_arg () {
    eval "echo \${$#}"  # this hurts
}

print_last_arg foo bar baz
# baz

(Nếu điều này là, giả sử, #!/usr/bin/zshthay vì #!/bin/shtôi biết phải làm gì. Vấn đề của tôi là tìm một cách hợp lý để thực hiện việc này #!/bin/sh.)

EDIT: Trên đây chỉ là một ví dụ ngớ ngẩn. Mục tiêu của tôi không phải là in đối số cuối cùng, mà là có một cách để tham chiếu đến đối số cuối cùng trong hàm shell.


EDIT2: Tôi xin lỗi vì một câu hỏi không rõ ràng như vậy. Tôi hy vọng sẽ có được nó ngay lúc này.

Nếu đây là /bin/zshthay vì /bin/sh, tôi có thể viết một cái gì đó như thế này

#!/bin/zsh

print_last_arg () {
    local last_arg=$argv[$#]
    echo $last_arg
}

Biểu thức $argv[$#]là một ví dụ về những gì tôi đã mô tả trong EDIT đầu tiên của mình như một cách để tham chiếu đến đối số cuối cùng trong hàm shell .

Vì vậy, tôi thực sự nên viết ví dụ ban đầu của mình như thế này:

print_last_arg () {
    local last_arg=$(eval "echo \${$#}")   # but this hurts even more
    echo $last_arg
}

... Để làm rõ rằng những gì tôi theo đuổi là một điều ít tệ hại hơn để đặt bên phải của nhiệm vụ.

Tuy nhiên, lưu ý rằng trong tất cả các ví dụ, đối số cuối cùng được truy cập không mang tính hủy diệt . IOW, việc truy cập của đối số cuối cùng khiến cho các đối số vị trí không bị ảnh hưởng.


unix.stackexchange.com/q/145522/117549 chỉ ra các khả năng khác nhau của #! / bin / sh - bạn có thể thu hẹp nó không?
Jeff Schaller

Tôi thích chỉnh sửa, nhưng có lẽ bạn cũng nhận thấy rằng có một câu trả lời ở đây cung cấp một phương tiện không phá hủy để tham chiếu đối số cuối cùng ...? bạn không nên đánh đồng var=$( eval echo \${$#})để eval var=\${$#}- hai là không có gì giống nhau.
mikeerv

1
Không chắc chắn tôi nhận được ghi chú cuối cùng của bạn nhưng hầu như tất cả các câu trả lời được cung cấp cho đến nay đều không phá hủy theo nghĩa chúng bảo tồn các đối số tập lệnh đang chạy. Các giải pháp duy nhất shiftset -- ...dựa trên có thể bị phá hủy trừ khi được sử dụng trong các chức năng nơi chúng cũng vô hại.
jlliagre

@jlliagre - nhưng chúng vẫn mang tính hủy diệt trong chính - chúng yêu cầu tạo bối cảnh dùng một lần để chúng có thể phá hủy để khám phá. nhưng ... nếu bạn nhận được một bối cảnh thứ hai - tại sao không chỉ có một bối cảnh cho phép bạn lập chỉ mục? Có điều gì sai với việc sử dụng công cụ dành cho công việc? diễn giải mở rộng shell như đầu vào có thể mở rộng là công việc của eval. và không có gì khác biệt đáng kể eval "var=\${$#}"khi làm so với var=${arr[evaled index]}ngoại trừ đó $#là một giá trị an toàn được đảm bảo. Tại sao sao chép toàn bộ sau đó hủy nó khi bạn có thể lập chỉ mục trực tiếp?
mikeerv

1
@mikeerv Một vòng lặp for được thực hiện trong phần chính của shell sẽ không thay đổi tất cả các đối số. Tôi đồng ý lặp tất cả các đối số là không được tối ưu hóa, đặc biệt là nếu hàng ngàn trong số chúng được chuyển sang shell và tôi cũng đồng ý rằng việc truy cập trực tiếp vào đối số cuối cùng với chỉ mục phù hợp là câu trả lời tốt nhất (và tôi không hiểu tại sao nó bị hạ cấp ) nhưng ngoài ra, không có gì thực sự phá hoại và không có bối cảnh bổ sung được tạo ra.
jlliagre

Câu trả lời:


1
eval printf %s${1+"'\n' \"the last arg is \${$#"\}\"}

... sẽ in chuỗi the last arg istheo sau là <dấu cách> , giá trị của đối số cuối cùng và dấu <newline> nếu có ít nhất 1 đối số, hoặc nếu không, đối với số không, thì nó sẽ không in gì cả.

Nếu bạn đã làm:

eval ${1+"lastarg=\${$#"\}}

... Sau đó, hoặc bạn sẽ gán giá trị của đối số cuối cùng cho biến shell $lastargnếu có ít nhất 1 đối số hoặc bạn sẽ không làm gì cả. Dù bằng cách nào, bạn sẽ làm điều đó một cách an toàn, và nó nên được mang theo ngay cả với vỏ Olde Bourne, tôi nghĩ vậy.

Đây là một cái khác sẽ hoạt động tương tự, mặc dù nó yêu cầu sao chép toàn bộ mảng arg hai lần (và yêu cầu một printftrong $PATHvỏ Bourne) :

if   [ "${1+:}" ]
then set "$#" "$@" "$@"
     shift    "$1"
     printf %s\\n "$1"
     shift
fi

Bình luận không dành cho thảo luận mở rộng; cuộc trò chuyện này đã được chuyển sang trò chuyện .
terdon

2
FYI, đề xuất đầu tiên của bạn thất bại dưới lớp vỏ bourne kế thừa với lỗi "thay thế xấu" nếu không có đối số. Cả hai cái còn lại làm việc như mong đợi.
jlliagre

@jillagre - cảm ơn bạn. tôi không chắc lắm về cái đầu đó - nhưng khá chắc chắn về hai cái kia. nó chỉ có nghĩa là để chứng minh làm thế nào các đối số có thể được truy cập bởi nội tuyến tham chiếu. tốt nhất là một chức năng sẽ mở ra với một cái gì đó giống như ${1+":"} returnngay lập tức - bởi vì ai muốn nó làm bất cứ điều gì hoặc có nguy cơ tác dụng phụ của bất kỳ loại nào nếu được gọi là không có gì? Toán học cũng khá giống nhau - nếu bạn có thể chắc chắn rằng bạn có thể mở rộng một số nguyên dương bạn có thể evalcả ngày. $OPTINDlà tuyệt vời cho nó.
mikeerv

3

Đây là một cách đơn giản:

print_last_arg () {
  if [ "$#" -gt 0 ]
  then
    s=$(( $# - 1 ))
  else
    s=0
  fi
  shift "$s"
  echo "$1"
}

(được cập nhật dựa trên quan điểm của @ cuonglm rằng bản gốc không thành công khi không có đối số; giờ đây nó lặp lại một dòng trống - thay đổi hành vi đó trong elsemệnh đề nếu muốn)


Điều này yêu cầu sử dụng / usr / xpg4 / bin / sh trên Solaris; cho tôi biết nếu Solaris #! / bin / sh là một yêu cầu.
Jeff Schaller

Câu hỏi này không phải là về một kịch bản cụ thể mà tôi đang cố viết, mà là một cách làm chung. Tôi đang tìm kiếm một công thức tốt cho việc này. Tất nhiên, càng di động, càng tốt.
kjo

toán học $ (()) không thành công trong Solaris / bin / sh; sử dụng cái gì đó như: shift `expr $ # - 1` nếu điều đó là bắt buộc.
Jeff Schaller

thay thế cho Solaris / bin / sh,echo "$# - 1" | bc
Jeff Schaller

1
đó chính xác là những gì tôi muốn viết, nhưng nó đã thất bại ở lớp vỏ thứ 2 mà tôi đã thử - NetBSD, rõ ràng là lớp vỏ Almquist không hỗ trợ nó :(
Jeff Schaller

3

Lấy ví dụ về bài mở đầu (đối số vị trí không có khoảng trắng):

print_last_arg foo bar baz

Đối với mặc định IFS=' \t\n', làm thế nào về:

args="$*" && printf '%s\n' "${args##* }"

Để mở rộng an toàn hơn "$*", hãy đặt IFS (per @ StéphaneChazelas):

( IFS=' ' args="$*" && printf '%s\n' "${args##* }" )

Nhưng ở trên sẽ thất bại nếu các đối số vị trí của bạn có thể chứa khoảng trắng. Trong trường hợp đó, sử dụng cái này thay thế:

for a in "$@"; do : ; done && printf '%s\n' "$a"

Lưu ý rằng các kỹ thuật này tránh sử dụng evalvà không có tác dụng phụ.

Đã thử nghiệm tại shellcheck.net


1
Cái đầu tiên thất bại nếu đối số cuối cùng chứa không gian.
cuonglm

1
Lưu ý rằng cái đầu tiên cũng không hoạt động trong vỏ trước POSIX
cuonglm

@cuonglm Phát hiện tốt, quan sát chính xác của bạn được kết hợp ngay bây giờ.
AsymLabs

Nó cũng giả sử nhân vật đầu tiên $IFSlà một không gian.
Stéphane Chazelas

1
Sẽ là nếu không có $ IFS trong môi trường, không được chỉ định theo cách khác. Nhưng vì bạn cần thiết lập IFS hầu như mọi lúc bạn sử dụng toán tử split + global (không mở rộng phần mở rộng), nên một cách tiếp cận với việc xử lý IFS là đặt nó bất cứ khi nào bạn cần. Sẽ không có hại khi IFS=' 'ở đây chỉ để làm rõ rằng nó được sử dụng để mở rộng "$*".
Stéphane Chazelas

3

Mặc dù câu hỏi này chỉ mới hơn 2 tuổi, tôi nghĩ rằng tôi muốn chia sẻ một tùy chọn hơi phức tạp.

print_last_arg () {
    echo "${@:${#@}:${#@}}"
}

Hãy chạy nó

print_last_arg foo bar baz
baz

Bash mở rộng tham số shell .

Biên tập

Thậm chí ngắn gọn hơn: echo "${@: -1}"

(Nhớ không gian)

Nguồn

Đã thử nghiệm trên macOS 10.12.6 nhưng cũng sẽ trả về đối số cuối cùng trên hầu hết các hương vị * nix có sẵn ...

Đau ít ¯\_(ツ)_/¯


Đây phải là câu trả lời được chấp nhận. Thậm chí tốt hơn sẽ là: echo "${*: -1}"shellchecksẽ không phàn nàn về.
Tom Hale

4
Điều này sẽ không hoạt động với một POSIX sh đơn giản, ${array:n:m:}là một phần mở rộng. (câu hỏi đã đề cập rõ ràng /bin/sh)
ilkkachu

2

POSIXly:

while [ "$#" -gt 1 ]; do
  shift
done

printf '%s\n' "$1"

(Cách tiếp cận này cũng hoạt động trong vỏ Bourne cũ)

Với các công cụ tiêu chuẩn khác:

awk 'BEGIN{print ARGV[ARGC-1]}' "$@"

(Điều này sẽ không hoạt động với cũ awk, mà không có ARGV)


Trường hợp vẫn còn vỏ Bourne, awkcũng có thể là awk cũ không có ARGV.
Stéphane Chazelas

@ StéphaneChazelas: Đồng ý. Ít nhất nó đã làm việc với phiên bản riêng của Brian Kernighan. Bạn có lý tưởng nào không?
cuonglm

Điều này xóa các đối số. Làm thế nào để giữ chúng?.

@BinaryZebra: Sử dụng awkcách tiếp cận hoặc sử dụng forvòng lặp đơn giản khác như cách khác đã làm hoặc chuyển chúng đến một hàm.
cuonglm

2

Điều này sẽ hoạt động với bất kỳ lớp vỏ tuân thủ POSIX nào và cũng sẽ hoạt động với lớp vỏ SolarIX Bourne kế thừa POSIX trước:

do=;for do do :;done;printf "%s\n" "$do"

và đây là một chức năng dựa trên cùng một cách tiếp cận:

print_last_arg()
  if [ "$*" ]; then
    for do do :;done
    printf "%s\n" "$do"
  else
    echo
  fi

PS: đừng nói với tôi rằng tôi đã quên các dấu ngoặc nhọn quanh thân hàm ;-)


2
Bạn đã quên các dấu ngoặc nhọn quanh thân hàm ;-).

@BinaryZebra Tôi đã cảnh báo bạn ;-) Tôi không quên họ. Niềng răng là tùy chọn đáng ngạc nhiên ở đây.
jlliagre

1
@jlliagre Tôi, thực sự, đã được cảnh báo ;-): P ...... Và: chắc chắn!

Phần nào của đặc tả cú pháp cho phép điều này?
Tom Hale

@TomHale Các quy tắc ngữ pháp shell cho phép cả hai thiếu in ...trong một forvòng lặp và không có dấu ngoặc nhọn xung quanh một ifcâu lệnh được sử dụng làm thân hàm. Xem for_clausecompound_commandtrong pubs.opengroup.org/onlinepub/9699919799 .
jlliagre

2

Từ "Unix - Câu hỏi thường gặp"

(1)

unset last
if    [ $# -gt 0 ]
then  eval last=\${$#}
fi
echo  "$last"

Nếu số lượng đối số có thể bằng 0, thì đối số 0 $0(thường là tên của tập lệnh) sẽ được gán cho $last. Đó là lý do cho nếu.

(2)

unset last
for   last
do    :
done
echo  "$last"

(3)

for     i
do
        third_last=$second_last
        second_last=$last
        last=$i
done
echo    "$last"

Để tránh in một dòng trống khi không có đối số, hãy thay thế echo "$last"cho:

${last+false} || echo "${last}"

Một số lượng đối số bằng không được tránh bởi if [ $# -gt 0 ].

Đây không phải là một bản sao chính xác của những gì trong liên kết trong trang, một số cải tiến đã được thêm vào.


0

Điều này sẽ hoạt động trên tất cả các hệ thống perlđược cài đặt (vì vậy hầu hết các UNICE):

print_last_arg () {
    printf '%s\0' "$@" | perl -0ne 's/\0//;$l=$_;}{print "$l\n"'
}

Mẹo là sử dụng printfđể thêm một \0đối số sau mỗi đối số shell và sau đó perl-0công tắc thiết lập dấu tách bản ghi của nó thành NULL. Sau đó, chúng tôi lặp lại đầu vào, loại bỏ \0và lưu từng dòng kết thúc NULL như $l. Các ENDkhối (đó là những gì các }{là) sẽ được thực hiện sau khi tất cả đầu vào đã được đọc như vậy sẽ in các "dòng" cuối cùng: đối số vỏ ngoái.


@mikeerv ah, đúng, tôi đã không thử nghiệm với một dòng mới trong bất kỳ ngoại trừ đối số cuối cùng. Phiên bản chỉnh sửa sẽ hoạt động cho hầu hết mọi thứ nhưng có lẽ quá phức tạp cho một nhiệm vụ đơn giản như vậy.
terdon

vâng, gọi một thực thi bên ngoài (nếu nó chưa phải là một phần của logic) , đối với tôi, hơi nặng tay. nhưng nếu điểm là phải vượt qua lập luận rằng vào sed, awkhoặc perlsau đó nó có thể là thông tin giá trị nào. bạn vẫn có thể làm tương tự với sed -zcác phiên bản GNU cập nhật.
mikeerv

Tại sao bạn bị hạ cấp? điều này thật tốt ... tôi nghĩ sao?
mikeerv

0

Đây là một phiên bản sử dụng đệ quy. Không biết làm thế nào POSIX tuân thủ điều này mặc dù ...

print_last_arg()
{
    if [ $# -gt 1 ] ; then
        shift
        echo $( print_last_arg "$@" )
    else
        echo "$1"
    fi
}

0

Một khái niệm đơn giản, không số học, không vòng lặp, không eval , chỉ các chức năng.
Hãy nhớ rằng vỏ Bourne không có số học (cần thiết bên ngoài expr). Nếu muốn có được một số học miễn phí, evalsự lựa chọn miễn phí, đây là một lựa chọn. Các hàm cần có nghĩa là SVR3 trở lên (không ghi đè các tham số).
Nhìn bên dưới để biết phiên bản mạnh mẽ hơn với printf.

printlast(){
    shift "$1"
    echo "$1"
}

printlast "$#" "$@"          ### you may use ${1+"$@"} here to allow
                             ### a Bourne empty list of arguments,
                             ### but wait for a correct solution below.

Cấu trúc này để gọi printlastcố định , các đối số cần phải được đặt trong danh sách các đối số vỏ $1, $2vv (đối số stack) và cuộc gọi được thực hiện như được đưa ra.

Nếu danh sách các đối số cần được thay đổi, chỉ cần đặt chúng:

set -- o1e t2o t3r f4u
printlast "$#" "$@"

Hoặc tạo một hàm ( getlast) dễ sử dụng hơn có thể cho phép các đối số chung (nhưng không nhanh bằng, các đối số được truyền hai lần).

getlast(){ printlast "$#" "$@"; }
getlast o1e t2o t3r f4u

Xin lưu ý rằng các đối số (của getlast, hoặc tất cả được bao gồm trong $@printlast) có thể có khoảng trắng, dòng mới, v.v. Nhưng không phải là NUL.

Tốt hơn

Phiên bản này không in 0khi danh sách các đối số trống và sử dụng printf mạnh hơn (quay lại echonếu printfkhông có sẵn bên ngoài cho các shell cũ).

printlast(){ shift  "$1"; printf '%s' "$1"; }
getlast  ()  if     [ $# -gt 0 ]
             then   printlast "$#" "$@"
                    echo    ### optional if a trailing newline is wanted.
             fi
### The {} braces were removed on purpose.

getlast 1 2 3 4    # will print 4

Sử dụng EVAL.

Nếu shell Bourne thậm chí còn cũ hơn và không có chức năng, hoặc nếu vì lý do nào đó, sử dụng eval là tùy chọn duy nhất:

Để in giá trị:

if    [ $# -gt 0 ]
then  eval printf "'1%s\n'" \"\$\{$#\}\"
fi

Để đặt giá trị trong một biến:

if    [ $# -gt 0 ]
then  eval 'last_arg='\"\$\{$#\}\"
fi

Nếu điều đó cần được thực hiện trong một hàm, các đối số cần được sao chép vào hàm:

print_last_arg () {
    local last_arg                ### local only works on more modern shells.
    eval 'last_arg='\"\$\{$#\}\"
    echo "last_arg3=$last_arg"
}

print_last_arg "$@"               ### Shell arguments are sent to the function.

nó là tốt hơn nhưng nó nói không sử dụng một vòng lặp - nhưng nó có. một bản sao mảng là một vòng lặp . và với hai chức năng, nó sử dụng hai vòng lặp. Ngoài ra - có một số lý do lặp lại toàn bộ mảng nên được ưu tiên để lập chỉ mục cho nó eval?
mikeerv

1
Bạn có thấy cái nào for loophay while loopkhông?


1
Theo tiêu chuẩn của bạn, tôi đoán rằng tất cả các mã đều có các vòng lặp, thậm chí echo "Hello"có một vòng lặp vì CPU phải đọc các vị trí bộ nhớ trong một vòng lặp. Một lần nữa: mã của tôi không có vòng lặp thêm.

1
Tôi thực sự không quan tâm đến ý kiến ​​của bạn tốt hay xấu, nó đã được chứng minh là sai nhiều lần rồi. Một lần nữa: mã của tôi không có for, whilehoặc untilvòng lặp. Bạn có thấy bất kỳ for whilehoặc untilvòng lặp được viết trong mã?. Không?, Sau đó chỉ cần chấp nhận thực tế, nắm lấy nó và học hỏi.

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.