Jak napisać skrypt, który działa z argumentami lub bez?

13

Mam skrypt bash, który wygląda mniej więcej tak:

#!/bin/bash

if [ $1 = "--test" ] || [ $1 = "-t" ]; then
    echo "Testing..."
    testing="y"

else
    testing="n"
    echo "Not testing."

fi

Więc chcę, aby móc nie tylko uruchamiać go z ./script --testlub ./script -t, ale także bez argumentów (tylko ./script), ale najwyraźniej, jeśli zrobię to z bieżącym kodem, wynikiem będzie:

./script: line 3: [: =: unary operator expected
./script: line 3: [: =: unary operator expected
Not testing.

Jak więc go zaprogramować, aby uruchomienie go bez żadnych argumentów wystarczyło, aby elsenie zgłosić błędu? Co ja robię źle?

Peter Cordes
źródło
1
Potrzebujesz podwójnych nawiasów wokół swoich czeków. [[]]
Terrance
3
Zobacz pułapkę Bash nr 4
steeldriver
@Terrance nie potrzebujesz ich, chociaż powinieneś ich używać, jeśli celujesz w bash.
Ruslan

Odpowiedzi:

1

„Właściwy sposób” używania długich opcji w skryptach powłoki to narzędzie getopt GNU . Istnieją również getopts, które są wbudowane w bash , ale pozwalają tylko na krótkie opcje, takie jak -t. Kilka przykładów getoptużycia można znaleźć tutaj .

Oto skrypt, który pokazuje, jak podejdę do twojego pytania. Wyjaśnienie większości kroków jest dodawane jako komentarze w samym skrypcie.

#!/bin/bash

# GNU getopt allows long options. Letters in -o correspond to
# comma-separated list given in --long.

opts=$(getopt -o t --long test -- "$*")
test $? -ne 0 && exit 2 # error happened

set -- $opts # some would prefer using eval set -- "$opts"
# if theres -- as first argument, the script is called without
# option flags
if [ "$1" = "--"  ]; then
    echo "Not testing"
    testing="n"
    # Here we exit, and avoid ever getting to argument parsing loop
    # A more practical case would be to call a function here
    # that performs for no options case
    exit 1
fi

# Although this question asks only for one 
# option flag, the proper use of getopt is with while loop
# That's why it's done so - to show proper form.
while true; do
    case "$1" in
        # spaces are important in the case definition
        -t | --test ) testing="y"; echo "Testing" ;;
    esac
    # Loop will terminate if there's no more  
    # positional parameters to shift
    shift  || break
done

echo "Out of loop"

Z kilkoma uproszczeniami i usuniętymi komentarzami można to skrócić:

#!/bin/bash
opts=$(getopt -o t --long test -- "$*")
test $? -ne 0 && exit 2 # error happened
set -- $opts    
case "$1" in
    -t | --test ) testing="y"; echo "Testing";;
    --) testing="n"; echo "Not testing";;
esac
Sergiy Kolodyazhnyy
źródło
16

Kilka sposobów; dwa najbardziej oczywiste to:

  • Umieścić $1w cudzysłowach:if [ "$1" = "--test" ]
  • Sprawdź liczbę argumentów za pomocą $ #

Jeszcze lepiej, użyj getopts.

JayEye
źródło
2
+1 za korzystanie z getopts. To najlepsza droga.
Seth
3
Innym powszechnym idiomem jest [ "x$1" = "x--test" ]ponowne bronienie argumentów wiersza poleceń, które są poprawnymi operatorami dla [polecenia. (Jest to kolejny powód, dla którego [[ ]]preferowane jest: to część składni powłoki, a nie tylko wbudowane polecenie).
Peter Cordes
7

Musisz podać swoje zmienne wewnątrz warunku if. Zastąpić:

if [ $1 = "--test" ] || [ $1 = "-t" ]; then

z:

if [ "$1" = '--test' ] || [ "$1" = '-t' ]; then  

Wtedy to zadziała:

➜ ~ ./test.sh
Nie testuję .

Zawsze zawsze podwój swoje cytaty!

Seth
źródło
Czy pojedyncze cudzysłowy są niezbędne, czy można zamiast tego stosować cudzysłowy?
Dokładna odpowiedź zależy od używanej powłoki, ale z twojego pytania zakładam, że używasz potomka powłoki Bourne'a. Główną różnicą między pojedynczymi i podwójnymi cudzysłowami jest to, że zmienna ekspansja zachodzi wewnątrz podwójnych cudzysłowów, ale nie wewnątrz pojedynczych cudzysłowów: $ FOO = bar $ echo "$ FOO" bar $ echo '$ FOO' $ FOO (hmm .... może ” t format kodu w komentarzach ...)
JayEye
@ParanoidPanda Nie musisz używać pojedynczych cudzysłowów, nie, ale dobrą praktyką jest stosowanie ich w literałach łańcuchowych. W ten sposób nie będziesz zaskoczony, jeśli twoje ciągi znaków mają symbol bash, który chce się rozwinąć.
Seth
@ParanoidPanda Co powiedział @JayEye: W przypadku pojedynczych cudzysłowów na wyjściu nie ma foo=bar echo '$foo'wydruków $fooze zmienną ekspansją , ale zamiast tego foo=bar echo "$foo"drukuje bar.
EKons
4

Używasz bash. Bash ma świetną alternatywę dla [: [[. Dzięki [[nie musisz martwić się o wycenę i otrzymujesz znacznie więcej operatorów niż [, w tym odpowiednie wsparcie dla ||:

if [[ $1 = "--test" || $1 = "-t" ]]
then
    echo "Testing..."
    testing="y"
else
    testing="n"
    echo "Not testing."
fi

Lub możesz użyć wyrażeń regularnych:

if [[ $1 =~ ^(--test|-t) ]]
then
    echo "Testing..."
    testing="y"
else
    testing="n"
    echo "Not testing."
fi

Oczywiście należy zawsze wykonywać prawidłowe cytowanie, ale nie ma powodu, aby się go trzymać [podczas korzystania z bash.

muru
źródło
3

Aby sprawdzić, czy istnieją argumenty (ogólnie i odpowiednio wykonać odpowiednie czynności) Powinno to działać:

#!/bin/bash

check=$1

if [ -z "$check" ]; then
  echo "no arguments"
else echo "there was an argument"
fi
Jacob Vlijm
źródło
Dlaczego nie [ -z "$1" ]?
EKons
@ ΈρικΚωνσταντόπουλος W tym przypadku, oczywiście, zwykle jednak w skryptach wywołuję zmienną jeden raz, odwołuję się do niej w procedurach, które zwykle są więcej niż jeden raz. Postanowił zachować protokół w próbce.
Jacob Vlijm
W tej próbce wydaje się, że używasz $checkraz, przed jakimkolwiek shifts, więc lepiej byłoby użyć $1zamiast tego.
EKons
@ ΈρικΚωνσταντόπουλος Oczywiście nie, jak wspomniano, jest to próbka , którą można zastosować w skryptach. W skryptach złą praktyką jest powtarzanie tego samego w kółko.
Jacob Vlijm
Rozumiem, co masz na myśli, ale lepiej jest nie używać shebangs #!podczas dostarczania próbek (powinieneś opisać powłokę poza kodem).
EKons