Câu hỏi Làm thế nào để phân tích cú pháp đối số dòng lệnh trong Bash?


Nói, tôi có một kịch bản được gọi với dòng này:

./myscript -vfd ./foo/bar/someFile -o /fizz/someOtherFile

hoặc cái này:

./myscript -v -f -d -o /fizz/someOtherFile ./foo/bar/someFile 

Cách thức được chấp nhận là phân tích cú pháp này như thế nào trong mỗi trường hợp (hoặc một số kết hợp của cả hai) $v, $f$d tất cả sẽ được đặt thành true và $outFile sẽ bằng /fizz/someOtherFile ?


1349
2017-10-10 16:57


gốc


Đối với người dùng zsh, có một bản dựng sẵn có tên là zparseopts có thể thực hiện: zparseopts -D -E -M -- d=debug -debug=d Và có cả hai -d và --debug bên trong $debug mảng echo $+debug[1] sẽ trả về 0 hoặc 1 nếu một trong số đó được sử dụng. Tham khảo: zsh.org/mla/users/2011/msg00350.html - dezza


Các câu trả lời:


Phương pháp # 1: Sử dụng bash mà không cần getopt [s]

Hai cách phổ biến để chuyển đối số khóa-giá trị-cặp là:

Dấu cách không gian Bash (ví dụ: --option argument) (không có getopt [s])

Sử dụng ./myscript.sh -e conf -s /etc -l /usr/lib /etc/hosts

#!/bin/bash

POSITIONAL=()
while [[ $# -gt 0 ]]
do
key="$1"

case $key in
    -e|--extension)
    EXTENSION="$2"
    shift # past argument
    shift # past value
    ;;
    -s|--searchpath)
    SEARCHPATH="$2"
    shift # past argument
    shift # past value
    ;;
    -l|--lib)
    LIBPATH="$2"
    shift # past argument
    shift # past value
    ;;
    --default)
    DEFAULT=YES
    shift # past argument
    ;;
    *)    # unknown option
    POSITIONAL+=("$1") # save it in an array for later
    shift # past argument
    ;;
esac
done
set -- "${POSITIONAL[@]}" # restore positional parameters

echo FILE EXTENSION  = "${EXTENSION}"
echo SEARCH PATH     = "${SEARCHPATH}"
echo LIBRARY PATH    = "${LIBPATH}"
echo DEFAULT         = "${DEFAULT}"
echo "Number files in SEARCH PATH with EXTENSION:" $(ls -1 "${SEARCHPATH}"/*."${EXTENSION}" | wc -l)
if [[ -n $1 ]]; then
    echo "Last line of file specified as non-opt/last argument:"
    tail -1 "$1"
fi

Bash được phân cách bằng nhau (ví dụ: --option=argument) (không có getopt [s])

Sử dụng ./myscript.sh -e=conf -s=/etc -l=/usr/lib /etc/hosts

#!/bin/bash

for i in "$@"
do
case $i in
    -e=*|--extension=*)
    EXTENSION="${i#*=}"
    shift # past argument=value
    ;;
    -s=*|--searchpath=*)
    SEARCHPATH="${i#*=}"
    shift # past argument=value
    ;;
    -l=*|--lib=*)
    LIBPATH="${i#*=}"
    shift # past argument=value
    ;;
    --default)
    DEFAULT=YES
    shift # past argument with no value
    ;;
    *)
          # unknown option
    ;;
esac
done
echo "FILE EXTENSION  = ${EXTENSION}"
echo "SEARCH PATH     = ${SEARCHPATH}"
echo "LIBRARY PATH    = ${LIBPATH}"
echo "Number files in SEARCH PATH with EXTENSION:" $(ls -1 "${SEARCHPATH}"/*."${EXTENSION}" | wc -l)
if [[ -n $1 ]]; then
    echo "Last line of file specified as non-opt/last argument:"
    tail -1 $1
fi

Để hiểu rõ hơn ${i#*=} tìm kiếm "Loại bỏ chuỗi con" trong hướng dẫn này. Nó có chức năng tương đương với `sed 's/[^=]*=//' <<< "$i"` gọi một tiến trình con không cần thiết hoặc `echo "$i" | sed 's/[^=]*=//'` cuộc gọi nào hai các quy trình con không cần thiết.

Sử dụng getopt [s]

từ: http://mywiki.wooledge.org/BashFAQ/035#getopts

getopt (1) hạn chế (cũ hơn, tương đối gần đây getopt phiên bản):

  • không thể xử lý các đối số là các chuỗi rỗng
  • không thể xử lý các đối số với khoảng trắng được nhúng

Gần đây hơn getopt các phiên bản không có những hạn chế này.

Ngoài ra, gói POSIX (và các chương trình khác) getopts không có những giới hạn này. Đây là một đơn giản getopts thí dụ:

#!/bin/sh

# A POSIX variable
OPTIND=1         # Reset in case getopts has been used previously in the shell.

# Initialize our own variables:
output_file=""
verbose=0

while getopts "h?vf:" opt; do
    case "$opt" in
    h|\?)
        show_help
        exit 0
        ;;
    v)  verbose=1
        ;;
    f)  output_file=$OPTARG
        ;;
    esac
done

shift $((OPTIND-1))

[ "${1:-}" = "--" ] && shift

echo "verbose=$verbose, output_file='$output_file', Leftovers: $@"

# End of file

Những lợi thế của getopts là:

  1. Đó là di động hơn, và sẽ làm việc trong vỏ khác như dash.
  2. Nó có thể xử lý nhiều tùy chọn duy nhất như -vf filename theo cách Unix điển hình, tự động.

Những bất lợi của getopts là nó chỉ có thể xử lý các tùy chọn ngắn (-h, không phải --help) không có mã bổ sung.

Đây là một getopts hướng dẫn giải thích ý nghĩa của tất cả cú pháp và biến. Trong bash, đó cũng là help getopts, có thể là thông tin.


1948
2018-01-07 20:01



Điều này có thực sự đúng không? Theo Wikipedia có phiên bản nâng cao GNU mới hơn của getopt bao gồm tất cả các chức năng của getopts và sau đó, vài. man getopt trên Ubuntu 13.04 kết quả đầu ra getopt - parse command options (enhanced) như tên, vì vậy tôi cho rằng phiên bản nâng cao này là chuẩn bây giờ. - Livven
Đó là một cái gì đó là một cách nhất định trên hệ thống của bạn là một tiền đề rất yếu để cơ sở asumptions "là tiêu chuẩn" trên. - szablica
@Livven, đó getopt không phải là một tiện ích GNU, nó là một phần của util-linux. - Stephane Chazelas
Nếu bạn dùng -gt 0, xóa shift sau esac, tăng thêm tất cả shift 1 và thêm trường hợp này: *) break;; bạn có thể xử lý các đối số tùy chọn không. Ví dụ: pastebin.com/6DJ57HTc - Nicolas Mongrain-Lacombe
Bạn không echo –default. Trong ví dụ đầu tiên, tôi nhận thấy rằng nếu –default là đối số cuối cùng, nó không được xử lý (được coi là không tham gia), trừ khi while [[ $# -gt 1 ]] được đặt thành while [[ $# -gt 0 ]] - kolydart


Không có đề cập câu trả lời tăng cường getopt. Và câu trả lời được bình chọn hàng đầu gây hiểu nhầm: Nó bỏ qua -⁠vfd các tùy chọn ngắn kiểu (được yêu cầu bởi OP), các tùy chọn sau các đối số vị trí (cũng được yêu cầu bởi OP) và nó bỏ qua các lỗi phân tích cú pháp. Thay thế:

  • Sử dụng nâng cao getopt từ util-linux hoặc GNU glibc trước đây.1
  • Nó hoạt động với getopt_long() chức năng C của GNU glibc.
  • Đã tất cả các các tính năng phân biệt hữu ích (các tính năng khác không có chúng):
    • xử lý dấu cách, trích dẫn các ký tự và thậm chí là nhị phân trong các đối số2
    • nó có thể xử lý các tùy chọn ở cuối: script.sh -o outFile file1 file2 -v
    • cho phép =tùy chọn kiểu dài: script.sh --outfile=fileOut --infile fileIn
  • Đã quá già rồi3 không có hệ thống GNU nào bị thiếu (ví dụ: bất kỳ Linux nào có hệ thống này).
  • Bạn có thể kiểm tra sự tồn tại của nó với: getopt --test → giá trị trả về 4.
  • Khác getopt hoặc shell-builtin getopts được sử dụng hạn chế.

Các cuộc gọi sau

myscript -vfd ./foo/bar/someFile -o /fizz/someOtherFile
myscript -v -f -d -o/fizz/someOtherFile -- ./foo/bar/someFile
myscript --verbose --force --debug ./foo/bar/someFile -o/fizz/someOtherFile
myscript --output=/fizz/someOtherFile ./foo/bar/someFile -vfd
myscript ./foo/bar/someFile -df -v --output /fizz/someOtherFile

tất cả trở lại

verbose: y, force: y, debug: y, in: ./foo/bar/someFile, out: /fizz/someOtherFile

với những điều sau đây myscript

#!/bin/bash
# saner programming env: these switches turn some bugs into errors
set -o errexit -o pipefail -o noclobber -o nounset

! getopt --test > /dev/null 
if [[ ${PIPESTATUS[0]} -ne 4 ]]; then
    echo "I’m sorry, `getopt --test` failed in this environment."
    exit 1
fi

OPTIONS=dfo:v
LONGOPTS=debug,force,output:,verbose

# -use ! and PIPESTATUS to get exit code with errexit set
# -temporarily store output to be able to check for errors
# -activate quoting/enhanced mode (e.g. by writing out “--options”)
# -pass arguments only via   -- "$@"   to separate them correctly
! PARSED=$(getopt --options=$OPTIONS --longoptions=$LONGOPTS --name "$0" -- "$@")
if [[ ${PIPESTATUS[0]} -ne 0 ]]; then
    # e.g. return value is 1
    #  then getopt has complained about wrong arguments to stdout
    exit 2
fi
# read getopt’s output this way to handle the quoting right:
eval set -- "$PARSED"

d=n f=n v=n outFile=-
# now enjoy the options in order and nicely split until we see --
while true; do
    case "$1" in
        -d|--debug)
            d=y
            shift
            ;;
        -f|--force)
            f=y
            shift
            ;;
        -v|--verbose)
            v=y
            shift
            ;;
        -o|--output)
            outFile="$2"
            shift 2
            ;;
        --)
            shift
            break
            ;;
        *)
            echo "Programming error"
            exit 3
            ;;
    esac
done

# handle non-option arguments
if [[ $# -ne 1 ]]; then
    echo "$0: A single input file is required."
    exit 4
fi

echo "verbose: $v, force: $f, debug: $d, in: $1, out: $outFile"

1 getopt nâng cao có sẵn trên hầu hết các hệ thống bash, bao gồm Cygwin; trên OS X thử brew cài đặt gnu-getopt
2 POSIX exec() các quy ước không có cách nào đáng tin cậy để vượt qua nhị phân NULL trong các đối số dòng lệnh; các byte đó kết thúc sớm đối số
3 phiên bản đầu tiên được phát hành vào năm 1997 hoặc trước đó (tôi chỉ theo dõi nó trở lại năm 1997)


333
2018-04-20 17:47



Cám ơn vì cái này. Vừa được xác nhận từ bảng tính năng tại en.wikipedia.org/wiki/Getopts, nếu bạn cần hỗ trợ cho các tùy chọn dài và bạn không sử dụng Solaris, getopt là con đường để đi. - johncip
Tôi tin rằng báo trước duy nhất với getopt là nó không thể được sử dụng thuận tiện trong các kịch bản trình bao bọc, trong đó người dùng có thể có ít tùy chọn cụ thể cho tập lệnh trình bao bọc, và sau đó chuyển các tùy chọn không bao bọc-script vào gói thực thi, còn nguyên vẹn. Hãy nói rằng tôi có một grep wrapper được gọi mygrep và tôi có một lựa chọn --foo cụ thể để mygrepthì tôi không thể làm mygrep --foo -A 2và có -A 2 được tự động chuyển đến grep; tôi nhu cầu làm mygrep --foo -- -A 2. Đây là triển khai của tôi trên đầu trang của giải pháp của bạn. - Kaushal Modi
@bobpaul Câu lệnh của bạn về util-linux cũng sai và gây hiểu nhầm: gói được đánh dấu là "thiết yếu" trên Ubuntu / Debian. Như vậy, nó luôn được cài đặt. - Bạn đang nói về distro nào (nơi bạn nói nó cần được cài đặt đúng mục đích)? - Robert Siemer
@ Jason, getopt sẽ thất bại 'cho bạn'. Không cần phải tự kiểm tra. - Robert Siemer
@AlexYursha @Marcos @bobpaul Kịch bản cải tiến sử dụng errexit hay còn gọi là set -e khá thanh lịch ngay bây giờ. - Robert Siemer


từ : digitalpeer.com với những sửa đổi nhỏ

Sử dụng myscript.sh -p=my_prefix -s=dirname -l=libname

#!/bin/bash
for i in "$@"
do
case $i in
    -p=*|--prefix=*)
    PREFIX="${i#*=}"

    ;;
    -s=*|--searchpath=*)
    SEARCHPATH="${i#*=}"
    ;;
    -l=*|--lib=*)
    DIR="${i#*=}"
    ;;
    --default)
    DEFAULT=YES
    ;;
    *)
            # unknown option
    ;;
esac
done
echo PREFIX = ${PREFIX}
echo SEARCH PATH = ${SEARCHPATH}
echo DIRS = ${DIR}
echo DEFAULT = ${DEFAULT}

Để hiểu rõ hơn ${i#*=} tìm kiếm "Loại bỏ chuỗi con" trong hướng dẫn này. Nó có chức năng tương đương với `sed 's/[^=]*=//' <<< "$i"` gọi một tiến trình con không cần thiết hoặc `echo "$i" | sed 's/[^=]*=//'` cuộc gọi nào hai các quy trình con không cần thiết.


104
2017-11-13 10:31



Khéo léo! Mặc dù điều này sẽ không hoạt động đối với các đối số được phân cách bằng dấu cách à la mount -t tempfs .... Người ta có thể sửa lỗi này thông qua một thứ như while [ $# -ge 1 ]; do param=$1; shift; case $param in; -p) prefix=$1; shift;; v.v. - Tobias Kienzler
Điều này không thể xử lý -vfd phong cách kết hợp các tùy chọn ngắn. - Robert Siemer
liên kết bị hỏng! - bekur


getopt()/getopts() là một lựa chọn tốt. Bị đánh cắp từ đây:

Việc sử dụng đơn giản "getopt" được hiển thị trong tập lệnh mini này:

#!/bin/bash
echo "Before getopt"
for i
do
  echo $i
done
args=`getopt abc:d $*`
set -- $args
echo "After getopt"
for i
do
  echo "-->$i"
done

Những gì chúng tôi đã nói là bất kỳ -a,   b, -c hoặc -d sẽ được cho phép, nhưng đó là tiếp theo là một đối số ("c:" nói rằng).

Nếu chúng ta gọi nó là "g" và thử nó:

bash-2.05a$ ./g -abc foo
Before getopt
-abc
foo
After getopt
-->-a
-->-b
-->-c
-->foo
-->--

Chúng tôi bắt đầu với hai đối số và   "getopt" phá vỡ các tùy chọn và   đặt mỗi đối số của riêng mình. Nó cũng   thêm "--".


101
2017-10-10 17:03



Sử dụng $* bị hỏng sử dụng getopt. (Nó đặt đối số với dấu cách.) Xem câu trả lời của tôi để sử dụng phù hợp. - Robert Siemer
Tại sao bạn muốn làm cho nó phức tạp hơn? - SDsolar
@ Matt J, phần đầu tiên của kịch bản (cho i) sẽ có thể xử lý các đối số với các khoảng trống trong chúng nếu bạn sử dụng "$ i" thay vì $ i. Các getopts dường như không thể xử lý các đối số với không gian. Lợi thế của việc sử dụng getopt qua vòng lặp i là gì? - thebunnyrules


Với nguy cơ thêm một ví dụ khác để bỏ qua, đây là sơ đồ của tôi.

  • tay cầm -n arg và --name=arg
  • cho phép đối số ở cuối
  • cho thấy lỗi sane nếu bất cứ điều gì sai chính tả
  • tương thích, không sử dụng bashisms
  • có thể đọc được, không yêu cầu duy trì trạng thái trong vòng lặp

Hy vọng nó hữu ích cho ai đó.

while [ "$#" -gt 0 ]; do
  case "$1" in
    -n) name="$2"; shift 2;;
    -p) pidfile="$2"; shift 2;;
    -l) logfile="$2"; shift 2;;

    --name=*) name="${1#*=}"; shift 1;;
    --pidfile=*) pidfile="${1#*=}"; shift 1;;
    --logfile=*) logfile="${1#*=}"; shift 1;;
    --name|--pidfile|--logfile) echo "$1 requires an argument" >&2; exit 1;;

    -*) echo "unknown option: $1" >&2; exit 1;;
    *) handle_argument "$1"; shift 1;;
  esac
done

61
2017-07-15 23:43



Chức năng "handle_argument" là gì? - rhombidodecahedron
Xin lỗi về sự chậm trễ. Trong kịch bản lệnh của tôi, hàm handle_argument nhận tất cả các đối số không tùy chọn. Bạn có thể thay thế dòng đó bằng bất cứ thứ gì bạn muốn, có thể *) die "unrecognized argument: $1" hoặc thu thập các arg vào một biến *) args+="$1"; shift 1;;. - bronson
Kinh ngạc! Tôi đã thử nghiệm một vài câu trả lời, nhưng đây là một trong những chỉ làm việc cho tất cả các trường hợp, bao gồm nhiều thông số vị trí (cả trước và sau cờ) - Guilherme Garnier


Cách gọn gàng hơn

script.sh

#!/bin/bash

while [[ "$#" > 0 ]]; do case $1 in
  -d|--deploy) deploy="$2"; shift;;
  -u|--uglify) uglify=1;;
  *) echo "Unknown parameter passed: $1"; exit 1;;
esac; shift; done

echo "Should deploy? $deploy"
echo "Should uglify? $uglify"

Sử dụng:

./script.sh -d dev -u

# OR:

./script.sh --deploy dev --uglify

41
2017-11-20 12:28



Đây là những gì tôi đang làm. Phải while [[ "$#" > 1 ]] nếu tôi muốn hỗ trợ kết thúc dòng với cờ boolean ./script.sh --debug dev --uglify fast --verbose. Thí dụ: gist.github.com/hfossli/4368aa5a577742c3c9f9266ed214aa58 - hfossli
Tôi đã gửi yêu cầu chỉnh sửa. Tôi chỉ thử nghiệm này và nó hoạt động hoàn hảo. - hfossli
Wow! Đơn giản và sạch sẽ! Đây là cách tôi đang sử dụng này: gist.github.com/hfossli/4368aa5a577742c3c9f9266ed214aa58 - hfossli
Đây giống như đoạn mã tao nhã nhất mà tôi từng thấy. +1 - Harindaka
nó đẹp và đơn giản. kudoz - jitter


Tôi trễ khoảng 4 năm cho câu hỏi này, nhưng muốn trả lại. Tôi đã sử dụng các câu trả lời trước đó làm điểm bắt đầu để dọn dẹp phân tích cú pháp thông số adhoc cũ của mình. Sau đó tôi đã cấu trúc lại mã mẫu sau. Nó xử lý cả các tham số dài và ngắn, sử dụng các đối số được phân tách bằng dấu cách hoặc không gian, cũng như nhiều tham số ngắn được nhóm lại với nhau. Cuối cùng, nó sẽ chèn lại bất kỳ đối số phi param nào vào các biến $ 1, $ 2 ... Tôi hy vọng nó hữu ích.

#!/usr/bin/env bash

# NOTICE: Uncomment if your script depends on bashisms.
#if [ -z "$BASH_VERSION" ]; then bash $0 $@ ; exit $? ; fi

echo "Before"
for i ; do echo - $i ; done


# Code template for parsing command line parameters using only portable shell
# code, while handling both long and short params, handling '-f file' and
# '-f=file' style param data and also capturing non-parameters to be inserted
# back into the shell positional parameters.

while [ -n "$1" ]; do
        # Copy so we can modify it (can't modify $1)
        OPT="$1"
        # Detect argument termination
        if [ x"$OPT" = x"--" ]; then
                shift
                for OPT ; do
                        REMAINS="$REMAINS \"$OPT\""
                done
                break
        fi
        # Parse current opt
        while [ x"$OPT" != x"-" ] ; do
                case "$OPT" in
                        # Handle --flag=value opts like this
                        -c=* | --config=* )
                                CONFIGFILE="${OPT#*=}"
                                shift
                                ;;
                        # and --flag value opts like this
                        -c* | --config )
                                CONFIGFILE="$2"
                                shift
                                ;;
                        -f* | --force )
                                FORCE=true
                                ;;
                        -r* | --retry )
                                RETRY=true
                                ;;
                        # Anything unknown is recorded for later
                        * )
                                REMAINS="$REMAINS \"$OPT\""
                                break
                                ;;
                esac
                # Check for multiple short options
                # NOTICE: be sure to update this pattern to match valid options
                NEXTOPT="${OPT#-[cfr]}" # try removing single short opt
                if [ x"$OPT" != x"$NEXTOPT" ] ; then
                        OPT="-$NEXTOPT"  # multiple short opts, keep going
                else
                        break  # long form, exit inner loop
                fi
        done
        # Done with that param. move to next
        shift
done
# Set the non-parameters back into the positional parameters ($1 $2 ..)
eval set -- $REMAINS


echo -e "After: \n configfile='$CONFIGFILE' \n force='$FORCE' \n retry='$RETRY' \n remains='$REMAINS'"
for i ; do echo - $i ; done

36
2017-07-01 01:20



Mã này không thể xử lý các tùy chọn với các đối số như sau: -c1. Và việc sử dụng = để tách các tùy chọn ngắn khỏi các đối số của họ là không bình thường ... - Robert Siemer
Tôi gặp phải hai vấn đề với đoạn code hữu ích này: 1) "shift" trong trường hợp "-c = foo" kết thúc bằng việc ăn tham số tiếp theo; và 2) 'c' không nên được đưa vào mẫu "[cfr]" cho các tùy chọn ngắn gọn có thể kết hợp được. - sfnd


Câu trả lời của tôi chủ yếu dựa trên câu trả lời của Bruno Bronosky, nhưng tôi đã nghiền nát hai cách triển khai bash thuần túy của anh ta thành một thứ mà tôi sử dụng khá thường xuyên.

# As long as there is at least one more argument, keep looping
while [[ $# -gt 0 ]]; do
    key="$1"
    case "$key" in
        # This is a flag type option. Will catch either -f or --foo
        -f|--foo)
        FOO=1
        ;;
        # Also a flag type option. Will catch either -b or --bar
        -b|--bar)
        BAR=1
        ;;
        # This is an arg value type option. Will catch -o value or --output-file value
        -o|--output-file)
        shift # past the key and to the value
        OUTPUTFILE="$1"
        ;;
        # This is an arg=value type option. Will catch -o=value or --output-file=value
        -o=*|--output-file=*)
        # No need to shift here since the value is part of the same string
        OUTPUTFILE="${key#*=}"
        ;;
        *)
        # Do whatever you want with extra options
        echo "Unknown option '$key'"
        ;;
    esac
    # Shift after checking all the cases to get the next option
    shift
done

Điều này cho phép bạn có cả các tùy chọn / giá trị được phân cách bằng dấu cách, cũng như các giá trị được xác định bằng nhau.

Vì vậy, bạn có thể chạy tập lệnh của mình bằng cách sử dụng:

./myscript --foo -b -o /fizz/file.txt

cũng như:

./myscript -f --bar -o=/fizz/file.txt

và cả hai cần có cùng kết quả cuối cùng.

PROS:

  • Cho phép cả giá trị -arg = value và -arg

  • Làm việc với bất kỳ tên arg nào mà bạn có thể sử dụng trong bash

    • Có nghĩa là -a hoặc -arg hoặc --arg hoặc -a-r-g hoặc bất cứ điều gì
  • Tinh khiết bash. Không cần phải học / sử dụng getopt hoặc getopts

Nhược điểm:

  • Không thể kết hợp args

    • Có nghĩa là không có -abc. Bạn phải làm -a -b -c

Đây là những ưu điểm duy nhất tôi có thể nghĩ ra khỏi đỉnh đầu


21
2017-09-08 18:59