Bash:引用符付き引数を含むコマンドを関数に渡す

Bash:引用符付き引数を含むコマンドを関数に渡す

以下のbash機能があります。

function exe {
    echo -e "Execute: $1"
    # Loops every 3s, outputting '...' until command finished executing
    LOOP=0
    while true;
    do
        if ! [ $LOOP == 0 ]; then echo -e "..."; fi;
        sleep 3;
        LOOP=$LOOP+1
    done & ERROR="$($2 2>&1)" # Execute the command and capture output to variable

    status=$?
    kill $!; trap 'kill $!' SIGTERM

    if [ $status -ne 0 ];
    then
        echo -e "✖ Error" >&2
        echo -e "$ERROR" >&2
    else
        echo -e "✔ Success"
    fi
    return $status
}

目的は、次のように呼び出すことです。

exe "Update apt indexes" \
    "sudo apt-get update"

任意の出力:

Execute: Update apt indexes
...
...
...
...
✔ Success

渡されたコマンドで引用符で囲まれた文字列をパラメータとして使用しないと、正常に動作します。

たとえば、次は機能しません。

exe "Create self signed certificate" \
    "sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/apache2/ssl/apache.key -out /etc/apache2/ssl/apache.crt -subj \"/C=GB/ST=London/L=London/O=Company Ltd/OU=IT Department/CN=dev.domain.local\""

set -x は、上記のコマンドが実行のために次のコマンドに変換されることを示します。

sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/apache2/ssl/apache.key -out /etc/apache2/ssl/apache.crt -subj '"/C=GB/ST=London/L=London/O=Confetti' Celebrations Ltd/OU=IT 'Department/CN=dev.sign-in.confetti.local"'

これは、単一引用符をたくさん選択してコマンドを無効にするようです。

この制限のないバージョンが必要です。どんなアイデアがありますか?

===============

提案やその他のバグ修正後の最終コードは次のとおりです。

exe () {
    echo -e "Execute: $1"
    LOOP=0
    while true;
    do
        if ! [ $LOOP == 0 ]; then echo -e "..."; fi;
        sleep 3;
        LOOP=$((LOOP+1))
    done & ERROR=$("${@:2}" 2>&1)
    status=$?
    kill $!; trap 'kill $!' SIGTERM

    if [ $status -ne 0 ];
    then
        echo -e "✖ Error" >&2
        echo -e "$ERROR" >&2
    else
        echo -e "✔ Success"
    fi
    return $status
}

この関数は放浪設定シェルスクリプトの「美化器」として使用され、次のように呼び出すことができます。

exe "Update apt indexes" sudo apt-get update

出力は次のように表示されます。

Execute: Update apt indexes
...
...
...
...
✔ Success

3秒未満持続するコマンドには進行状況出力は表示されません。

エラーが発生しない限り、エラー状態とコマンドの完全な出力を受け取ります。

主な目的は、stderrにメッセージを出力するときに放浪設定スクリプトによって表示される赤い線を削除することです。多くのコマンドは、メッセージを別のコマンドにリンクしないことが目的であるため、stderrに情報を正しく出力します。 Vagrant はメッセージを標準出力に出力します。これはエラーのように見えますが、実際にはエラーではなく、多くの構成メッセージが残る可能性があります。

この関数は、実行されたコマンドがゼロ以外の状態を返さない限り、stderrに印刷しません。これは、コマンドが失敗を示さない限り、赤いメッセージが表示されないことを意味します。コマンドがゼロ以外のメッセージで失敗を示す場合は、コマンドの出力全体をstderrに印刷して赤い線を表示します。

シェルスクリプトを使用すると、放浪設定がはるかにきれいになり、実際に赤いメッセージを注意深く観察し、その意味を知ることができます。

上記のコードで見逃したいくつかのビジュアル要素を含むバグラントで使用できる完全な機能はここにあります。https://gist.github.com/michaelward82/c1903f2b37a76975740e

エラーなしでexe関数を使用した出力の例: エラーのあるexe関数を使用した出力例

エラーのあるexe関数を使用した出力の例: エラーのあるexe関数を使用した出力例

実際のエラーなしでコマンドを直接実行したデフォルト出力は次のとおりです。 コマンドのデフォルト出力は、実際のエラーなしで直接実行されます。

答え1

コマンド全体を文字列として渡したくない場合があります。シェルにはパラメータリストとしてリストがあり、リストをリストに渡す方がはるかに簡単です。

を書くのではなく、exe blah "blahh cmd"コマンドを直接作成し、exe blah blahh cmdコマンド全体を直接使用する必要がある場合はを使用してください。スライス拡張最初のパラメータに続くすべての項目を取得しますERROR=$("${@:1}" 2>&1)

shift伝統的にパラメータリスト全体を移動するには、「左」を使用できます(参考文献を参照help shift)。

f(){
    local j="$1"
    shift
    echo "$j,$3"
    shift 50
    echo "$1" # guess what "$@" is now?
}

f {1..100}

しかし、これは明らかにbashには必要ありません。

スライシングについて話したら、次も確認してみてください。ソートバッシュから。


ええと、それでも..を使用して文字列を直接実行できますが、eval単純なコマンド以上の機能を許可するため、これは通常悪いと見なされます。


スタイルのヒントとして、xxx()より短くてfunction xxx移植性の高い(POSIX-)を好みますfunction xxx()。 Bashでは同じです。

答え2

あなたの質問の重要な質問は、「文字列を分割する方法」です$var

「悪」(エラーやコードの実行が起こりやすいため)の方法はevalを使用することです。

 eval set -- $var           ### Dangerous, not recommended, do not use.

これは位置引数に分割文字列を設定します(配列は少し複雑です)。しかし$var、変数を引用解除すると(何をしているのか実際に知らない限り、どんな犠牲を払っても避けるべきです)、「単語の分離」(私たちが望むもの)が発生しますが、「パス名の拡張」も発生することがあります。このコマンドを試すことができます(ファイルがほとんどないディレクトリを使用)。

$ var='hello * world'
$ eval set -- $var
$ echo "$@"

実行は安全で、外部では値が設定されず、拡張機能は*位置引数の値のみを設定します。

「パス名の拡張」を防ぐためにset -faが使用され、この場合はコマンドに統合するのは簡単です。

$ var='hello * world'
$ set -f
$ eval set -- $var
$ echo "$@"
hello * world

デフォルトのIFSはですspaceTabNew Line

IFS を外部で設定できると、状況が複雑になる可能性があります。

次の方法を使用すると、さまざまな問題を解決できますread

$ IFS=' ' read -ra arr <<<"$var"
$ echo "${arr[@]}"
hello * world

これはコマンドのIFSを設定し(IFSを外部に設定しない)、読み取り時にバックスラッシュを処理せず(-rオプション)、すべてを配列変数に入れ(-aオプション)引用変数を使用します"$var"。注意すべき点は、単語間に繰り返される空白が削除されることです(IFSは空白なので)。実行可能なコマンドラインでは問題ありません。

ただし、スペースを含む引数を必要とするコマンドを実行しようとすると失敗します。

$ var='date -d "-1 day" +"%Y.%m.%d-%H:%M:%S"'
$ IFS=' ' read -ra arr <<<"$var"
$ "${arr[@]}"
date: extra operand `+"%Y.%m.%d-%H:%M:%S"'

唯一の実際の解決策は、最初からコマンド配列を正しく構築することです。

$ arr=( date -d "-1 day" +"%Y.%m.%d-%H:%M:%S" )
$ "${arr[@]}"
2016.03.05-00:25:17

このソリューションをCSV「カンマ(スペース)で区切られた値」と考えてください。

このスクリプトは次のように動作します。

#!/bin/bash

function exe {
    echo "Execute: $1"
    # Loops every 3s, outputting '...' until command finished executing
    LOOP=0
    while true; do
        if [ $LOOP -gt 0 ]; then echo -e "..."; fi;
            sleep 3;
            (( LOOP++ ))
    done &

    ERROR="$("${@:2}" 2>&1)" # Execute command and capture output.
    status=$?

    kill $!; trap 'kill $!' SIGTERM

    if [ $status -ne 0 ];
    then
        echo "✖ Error" >&2
        echo "$ERROR" >&2
    else
        echo "✔ Success"
    fi
    return $status
}

cmd=( date -d '-1 day' +'%Y.%m.%d-%H:%M:%S' )
exe "give me yesterday date" "${cmd[@]}" 

cmd=( sudo apt-get update )
exe "update package list" "${cmd[@]}" 

答え3

コードで実行するときにパラメータ文字列内に引用符がある場合は、パラメータ文字列を位置パラメータ配列と同じ配列に再解析できます$@。これは、少なくとも与えられた例では-を使用して達成することができます... & ERROR="$( printf "%s" "$2" | xargs sh -c 'exec "$0" "$@" 2>&1' ) ...。 (既に引用符付き文字列に追加の二重引用符がある場合は、xargs: unterminated quoteメッセージが表示されることがあります。)

追加のアドバイスについては、以下を参照してください。Linux/Bash: 引用を解除するには?

# test cases
# help :
#set -- '' "ls -ld / 'a bc'" 
set -- '' ": sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/apache2/ssl/apache.key -out /etc/apache2/ssl/apache.crt -subj \"/C=GB/ST=London/L=London/O=Company Ltd/OU=IT Department/CN=dev.domain.local\""

printf "%s" "$2" | 
    xargs sh -c '
       echo "arg 0: ${0}"
       for ((i=1; i<=$#; i++)); do
          echo "arg $i: ${@:i:1}"
       done
       set -xv
       "$0" "$@"
    ' 

# output
arg 0: :
arg 1: sudo
arg 2: openssl
arg 3: req
arg 4: -x509
arg 5: -nodes
arg 6: -days
arg 7: 365
arg 8: -newkey
arg 9: rsa:2048
arg 10: -keyout
arg 11: /etc/apache2/ssl/apache.key
arg 12: -out
arg 13: /etc/apache2/ssl/apache.crt
arg 14: -subj
arg 15: /C=GB/ST=London/L=London/O=Company Ltd/OU=IT Department/CN=dev.domain.local
   "$0" "$@"
+ : sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/apache2/ssl/apache.key -out /etc/apache2/ssl/apache.crt -subj '/C=GB/ST=London/L=London/O=Company Ltd/OU=IT Department/CN=dev.domain.local'

(BTWLOOP=$LOOP+1上記のコードでは必要ですLOOP=$((LOOP+1))。)

関連情報