簡體   English   中英

POSIX sh 等效於 Bash 的 printf %q

[英]POSIX sh equivalent for Bash’s printf %q

假設我有一個#!/bin/sh腳本,它可以采用各種位置參數,其中一些可能包括空格、兩種/兩種引號等。我想迭代"$@"並為每個參數處理它立即以某種方式,或保存以備后用。 在腳本結束時,我想啟動(可能是exec )另一個進程,傳入其中一些參數,並且所有特殊字符都完好無損。

如果我不對參數做任何處理, othercmd "$@"可以正常工作,但我需要提取一些參數並稍微處理一下。

如果我可以假設 Bash,那么我可以使用printf %q來計算我可以稍后eval的 args 的引用版本,但這不適用於例如 Ubuntu 的 Dash ( /bin/sh )。

是否有任何等效於printf %q的可以用普通的 Bourne shell 腳本編寫,僅使用內置和 POSIX 定義的實用程序,比如 ZC1C425268E68385D14AB5074C17Z9 可以復制到腳本中?

例如,一個腳本試圖以相反的順序ls其 arguments:

#!/bin/sh
args=
for arg in "$@"
do
    args="'$arg' $args"
done
eval "ls $args"

適用於許多情況:

$ ./handle goodbye "cruel world"
ls: cannot access cruel world: No such file or directory
ls: cannot access goodbye: No such file or directory

但不是在使用'時:

$ ./handle goodbye "cruel'st world"
./handle: 1: eval: Syntax error: Unterminated quoted string

以下工作正常,但依賴於 Bash:

#!/bin/bash
args=
for arg in "$@"
do
    printf -v argq '%q' "$arg"
    args="$argq $args"
done
eval "ls $args"

這絕對可行。

你在Jesse Glick看到的答案就在那里,但它有一些錯誤,我還有一些替代方案供你考慮,因為這是我遇到的問題不止一次。

首先,您可能已經知道這一點,回聲是一個壞主意,如果目標是可移植性,應該使用printf:如果接收的參數是“-n”,則“echo”在POSIX中具有未定義的行為,並且在實踐中echo的實現將-n作為特殊選項,而其他人只是將其視為打印的普通參數。 所以這就變成了:

esceval()
{
    printf %s "$1" | sed "s/'/'\"'\"'/g"
}

或者,不要通過將嵌入的單引號轉換為:

'"'"'

..而你可以把它們變成:

'\''

我猜想(我認為性能差異可以忽略不計,盡管我從未測試過)。 生成的sed字符串如下所示:

esceval()
{
    printf %s "$1" | sed "s/'/'\\\\''/g"
}

(它是四個反斜杠,因為雙引號吞下其中兩個,然后留下兩個,然后sed吞下一個,只留下一個。就個人而言,我發現這種方式更具可讀性,這就是我將在其余涉及的示例中使用的內容它,但兩者都應該是等價的。)

但是,我們仍然有一個錯誤:命令替換將從命令輸出中刪除至少一個(但在許多shell ALL中)尾隨換行符(並非所有空格,特別是換行符)。 所以上述解決方案是有效的,除非你在參數的最后有新行。 然后你會丟失/那些換行符。 修復顯然很簡單:在從quote / esceval函數輸出之前,在實際命令值之后添加另一個字符。 順便說一句,我們無論如何都需要這樣做,因為我們需要使用單引號啟動和停止轉義參數。 老實說,我不明白為什么不開始這樣做。 你有兩個選擇:

esceval()
{
    printf '%s\n' "$1" | sed "s/'/'\\\\''/g; 1 s/^/'/; $ s/$/'/"
}

這將確保參數已經完全轉義,在構建最終字符串時無需添加更多單引號。 這可能是您將獲得單個內聯版本的最接近的內容。 如果您對sed依賴項沒問題,可以在此處停止。

如果你對sed依賴關系不好,但你可以假設你的shell實際上是POSIX兼容的(那里還有一些,特別是Solaris 10及更低版本的/ bin / sh,這不會能夠做到這個下一個變體 - 但幾乎所有你需要關心的shell都可以做到這一點:

esceval()
{
    printf \'
    UNESCAPED=$1
    while :
    do
        case $UNESCAPED in
        *\'*)
            printf %s "${UNESCAPED%%\'*}""'\''"
            UNESCAPED=${UNESCAPED#*\'}
            ;;
        *)
            printf %s "$UNESCAPED"
            break
        esac
    done
    printf \'
}

您可能會注意到這里看似多余的引用:

printf %s "${UNESCAPED%%\'*}""'\''"

..這可以替換為:

printf %s "${UNESCAPED%%\'*}'\''"

我做前者的唯一原因是因為曾經有一個Bourne shell在將變量替換為帶引號的字符串時有錯誤,其中變量周圍的引用並不完全開始和結束變量替換所做的。 因此,這是我的一種偏執的便攜性習慣。 在實踐中,你可以做后者,這不會是一個問題。

如果您不想在shell環境的其余部分中破壞變量UNESCAPED,那么您可以將該函數的全部內容包裝在子shell中,如下所示:

esceval()
{
  (
    printf \'
    UNESCAPED=$1
    while :
    do
        case $UNESCAPED in
        *\'*)
            printf %s "${UNESCAPED%%\'*}""'\''"
            UNESCAPED=${UNESCAPED#*\'}
            ;;
        *)
            printf %s "$UNESCAPED"
            break
        esac
    done
    printf \'
  )
}

“但等等”,你說:“我想在一個命令中對MULTIPLE參數做什么呢?我希望輸出對我來說仍然看起來有點好看,如果我因為某種原因從命令行運行它“。

永遠不要害怕,我告訴你:

esceval()
{
    case $# in 0) return 0; esac
    while :
    do
        printf "'"
        printf %s "$1" | sed "s/'/'\\\\''/g"
        shift
        case $# in 0) break; esac
        printf "' "
    done
    printf "'\n"
}

..或同樣的事情,但只有shell版本:

esceval()
{
  case $# in 0) return 0; esac
  (
    while :
    do
        printf "'"
        UNESCAPED=$1
        while :
        do
            case $UNESCAPED in
            *\'*)
                printf %s "${UNESCAPED%%\'*}""'\''"
                UNESCAPED=${UNESCAPED#*\'}
                ;;
            *)
                printf %s "$UNESCAPED"
                break
            esac
        done
        shift
        case $# in 0) break; esac
        printf "' "
    done
    printf "'\n"
  )
}

在最后四個中,你可以折疊一些外部printf語句並將它們的單引號轉換為另一個printf - 我將它們分開,因為我覺得當你可以在單獨的單引號上看到它時邏輯更清晰打印報表。

PS我也做了這個怪物,這是一個polyfill,可以在前兩個版本之間進行選擇,具體取決於你的shell是否能夠支持必要的變量替換語法(雖然它看起來很糟糕,因為shell-only版本必須在eval-ed字符串中,以防止不兼容的shell在看到它時發生barfing): https//github.com/mentalisttraceur/esceval/blob/master/sh/esceval.sh

我認為這是POSIX。 它通過在為for循環擴展它之后清除$@ ,但只有一次,以便我們可以使用set迭代地(反向)構建它。

flag=0
for i in "$@"; do
    [ "$flag" -eq 0 ] && shift $#
    set -- "$i" "$@"
    flag=1
done

echo "$@"   # To see that "$@" has indeed been reversed
ls "$@"

我意識到反轉參數僅僅是一個例子,但你可以在其他情況下使用set -- "$arg" "$@"這個技巧set -- "$arg" "$@"set -- "$@" "$arg"

是的,我意識到我可能剛剛重新實現了(很差)ormaaj的Push。

有關示例,請參閱自述文件。

以下似乎適用於我迄今為止所做的一切,包括空格,兩種引號和各種其他元字符,以及嵌入的換行符:

#!/bin/sh
quote() {
    echo "$1" | sed "s/'/'\"'\"'/g"
}
args=
for arg in "$@"
do
    argq="'"`quote "$arg"`"'"
    args="$argq $args"
done
eval "ls $args"

如果您可以調用外部可執行文件(如在其他答案中給出的sed解決方案中),那么您也可以調用/usr/bin/printf 雖然 POSIX shell 內置printf確實不支持%q ,但 Coreutils 的printf二進制文件確實支持( 從 8.25 版開始)。

esceval() {
    /usr/bin/printf '%q ' "$@"
}

當 GNU Coreutil 版本不低於 8.25 時,我們可以使用 /usr/bin/printf

#!/bin/sh

minversion="8.25"
gnuversion=$(ls '--version' | sed '1q' | awk 'NF{print $NF}')

printcmd="printf"

if ! [ $gnuversion \< $minversion ]; then
    printcmd="/usr/bin/printf"
fi;

params=$($printcmd "%q" "$@")

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM