尽管有评论,但没有直接的问题tcsh
(相信我,我不是 C shell 的粉丝),bash
本身也没有问题。实际上,如果您将 替换为 ,问题将是相似tcsh
的bash
。
问题是你想要做的事情实际上是非常难以做到的。让我解释...
在bash
脚本中,您试图创建一个字符串,该字符串将包含一个tcsh
可以正确解释的有效命令行,包括在参数中保留空格。
逐步制定答案
让我们从一些简单的东西开始——其中没有空格的参数:
set -- /bin/ls /bin/sh /bin/bash # Set the arguments to bash
/bin/tcsh -c "ls -l $*"
这将正常工作;它将执行 C shell,C shell 将处理字符串并执行:
ls -l /bin/ls /bin/sh /bin/bash
因此,问题是当整个命令被指定为单个字符串时,如何可靠地将带有空格的参数传递给 C shell。
您已经知道这会遇到问题:
mkdir "./a b c" "./d e f"
set -- "a b c" "d e f" # Two arguments with spaces
/bin/tcsh -c "ls -al $*"
在我的机器上,我得到:
ls: a: No such file or directory
ls: b: No such file or directory
ls: c: No such file or directory
ls: d: No such file or directory
ls: e: No such file or directory
ls: f: No such file or directory
如果我们手动进行扩展,我们可以通过以下方式获得所需的结果(对于这个有限的示例):
mkdir "./a b c" "./d e f"
set -- "a b c" "d e f" # Two arguments with spaces
/bin/tcsh -c "ls -al 'a b c' 'd e f'"
这产生:
a b c:
total 0
drwxr-xr-x 2 jleffler staff 68 Aug 25 12:21 .
drwxr-xr-x 4 jleffler staff 136 Aug 25 12:21 ..
d e f:
total 0
drwxr-xr-x 2 jleffler staff 68 Aug 25 12:21 .
drwxr-xr-x 4 jleffler staff 136 Aug 25 12:21 ..
(我将假设两个目录 ' a b c
' 和 ' d e f
' 从这里开始就存在,而不是每次都创建它们。)
因此,目标必须是找到一种方法来创建一个在由 C shell 解释时将是安全的字符串,自动(而不是手动显示)。由于 C shell 具有元句法动物园(许多特殊字符),完整的任务会很困难,但让我们先完成简单的事情——空格和没有元字符。
对于每个参数,我们希望在开头和结尾添加单引号,并确保字符串中的任何单引号都受到保护。那是它自己的小聚会;诀窍是用第一个单引号结束当前单引号字符串的序列替换嵌入的单引号'\''
,反斜杠单引号嵌入一个单引号,最后一个单引号开始一个新的单引号字符串。我们希望将其添加到当前命令字符串的末尾。因此,这导致:
set -- "a b c" "d e f" # Two arguments with spaces
cmd="ls -al"
for arg in "$@"
do escaped=$(sed -e "s/'/'\\''/g" -e "s/^/'/" -e "s/$/'/" <<< "$arg")
cmd="$cmd $escaped"
done
echo "$cmd"
tcsh -c "$cmd"
这会产生(当然,该ls
行来自echo
):
ls -al 'a b c' 'd e f'
a b c:
total 0
drwxr-xr-x 2 jleffler staff 68 Aug 25 12:21 .
drwxr-xr-x 4 jleffler staff 136 Aug 25 12:21 ..
d e f:
total 0
drwxr-xr-x 2 jleffler staff 68 Aug 25 12:21 .
drwxr-xr-x 4 jleffler staff 136 Aug 25 12:21 ..
好的,到目前为止,一切都很好。那么元句法动物园呢?幸运的是,大多数字符在单引号内没有特殊含义。
是时候将一些更复杂的目录添加到列表中了(这些目录也会在问题的持续时间内存在)。确保您知道正在创建的名称;你需要很好地理解 shell 引用。
作为练习,对于在此问题中创建的每个目录名称,写出用单引号括起来、用双引号括起来并且整个参数周围没有任何引号时给出相同结果的替代方案。
$ mkdir '! % *' '$(pwd)' '`pwd`'
脚本几乎没有变化——它使用 shell glob 生成目录名称列表,依次回显每个参数,并列出 inode 编号:
set -- *
cmd="ls -ail"
for arg in "$@"
do echo "arg: $arg"
escaped=$(sed -e "s/'/'\\''/g" -e "s/^/'/" -e "s/$/'/" <<< "$arg")
cmd="$cmd $escaped"
done
echo "cmd: $cmd"
tcsh -c "$cmd"
嘿,快点:
arg: ! % *
arg: $(pwd)
arg: `pwd`
arg: a b c
arg: d e f
cmd: ls -ail '! % *' '$(pwd)' '`pwd`' 'a b c' 'd e f'
! % *:
total 0
1640119 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:34 .
1640040 drwxr-xr-x 7 jleffler staff 238 Aug 25 12:34 ..
$(pwd):
total 0
1640120 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:34 .
1640040 drwxr-xr-x 7 jleffler staff 238 Aug 25 12:34 ..
`pwd`:
total 0
1640121 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:34 .
1640040 drwxr-xr-x 7 jleffler staff 238 Aug 25 12:34 ..
a b c:
total 0
1640056 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:21 .
1640040 drwxr-xr-x 7 jleffler staff 238 Aug 25 12:34 ..
d e f:
total 0
1640057 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:21 .
1640040 drwxr-xr-x 7 jleffler staff 238 Aug 25 12:34 ..
正是医生吩咐的!但我们还不够残酷:就像 Knuth 所说,当你测试代码时,你必须进入一种非常讨厌的卑鄙心态,所以让我们尝试一下:
$ mkdir "O'Reilly's Books"
$ mkdir "' \` \""
$ mkdir '${HOME}' '$PATH' 'He said, "Don'\''t Do It!"'
$ ls -l
total 0
drwxr-xr-x 2 jleffler staff 68 Aug 25 12:34 ! % *
drwxr-xr-x 2 jleffler staff 68 Aug 25 12:34 $(pwd)
drwxr-xr-x 2 jleffler staff 68 Aug 25 12:45 $PATH
drwxr-xr-x 2 jleffler staff 68 Aug 25 12:45 ${HOME}
drwxr-xr-x 2 jleffler staff 68 Aug 25 12:43 ' ` "
drwxr-xr-x 2 jleffler staff 68 Aug 25 12:45 He said, "Don't Do It!"
drwxr-xr-x 2 jleffler staff 68 Aug 25 12:43 O'Reilly's Books
drwxr-xr-x 2 jleffler staff 68 Aug 25 12:34 `pwd`
drwxr-xr-x 2 jleffler staff 68 Aug 25 12:21 a b c
drwxr-xr-x 2 jleffler staff 68 Aug 25 12:21 d e f
$
结果是:
arg: ! % *
arg: $(pwd)
arg: $PATH
arg: ${HOME}
arg: ' ` "
arg: He said, "Don't Do It!"
arg: O'Reilly's Books
arg: `pwd`
arg: a b c
arg: d e f
cmd: ls -ail '! % *' '$(pwd)' '$PATH' '${HOME}' '''' ` "' 'He said, "Don'''t Do It!"' 'O'''Reilly'''s Books' '`pwd`' 'a b c' 'd e f'
Unmatched `.
那不是我们想要的。但是,部分麻烦在于标记为 ' cmd:
' 的行中的 4 个单引号序列;它应该是''\''
。所以,sed
剧本不够准确。
set -- *
cmd="ls -ail"
for arg in "$@"
do echo "arg: $arg"
escaped=$(sed -e "s/'/'\\\\''/g" -e "s/^/'/" -e "s/$/'/" <<< "$arg")
cmd="$cmd $escaped"
done
echo "cmd: $cmd"
tcsh -c "$cmd"
当它运行时,我们得到:
arg: ! % *
arg: $(pwd)
arg: $PATH
arg: ${HOME}
arg: ' ` "
arg: He said, "Don't Do It!"
arg: O'Reilly's Books
arg: `pwd`
arg: a b c
arg: d e f
arg: x.sh
cmd: ls -ail '! % *' '$(pwd)' '$PATH' '${HOME}' ''\'' ` "' 'He said, "Don'\''t Do It!"' 'O'\''Reilly'\''s Books' '`pwd`' 'a b c' 'd e f' 'x.sh'
1640231 -rw-r--r-- 1 jleffler staff 223 Aug 25 12:56 x.sh
! % *:
total 0
1640119 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:34 .
1640040 drwxr-xr-x 13 jleffler staff 442 Aug 25 12:56 ..
$(pwd):
total 0
1640120 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:34 .
1640040 drwxr-xr-x 13 jleffler staff 442 Aug 25 12:56 ..
$PATH:
total 0
1640176 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:45 .
1640040 drwxr-xr-x 13 jleffler staff 442 Aug 25 12:56 ..
${HOME}:
total 0
1640175 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:45 .
1640040 drwxr-xr-x 13 jleffler staff 442 Aug 25 12:56 ..
' ` ":
total 0
1640163 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:43 .
1640040 drwxr-xr-x 13 jleffler staff 442 Aug 25 12:56 ..
He said, "Don't Do It!":
total 0
1640177 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:45 .
1640040 drwxr-xr-x 13 jleffler staff 442 Aug 25 12:56 ..
O'Reilly's Books:
total 0
1640164 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:43 .
1640040 drwxr-xr-x 13 jleffler staff 442 Aug 25 12:56 ..
`pwd`:
total 0
1640121 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:34 .
1640040 drwxr-xr-x 13 jleffler staff 442 Aug 25 12:56 ..
a b c:
total 0
1640056 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:21 .
1640040 drwxr-xr-x 13 jleffler staff 442 Aug 25 12:56 ..
d e f:
total 0
1640057 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:21 .
1640040 drwxr-xr-x 13 jleffler staff 442 Aug 25 12:56 ..
够意思吗?越来越接近。包含反斜杠的目录名呢?
$ mkdir "a \\' \\\` \\$ b \\\" c" # Make sure you do the exercise!
$ mkdir 'a \\'\'' \\\` \\$ b \\\" c' # Make sure you do the exercise!
$ ls -li
total 8
1640119 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:34 ! % *
1640120 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:34 $(pwd)
1640176 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:45 $PATH
1640175 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:45 ${HOME}
1640163 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:43 ' ` "
1640177 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:45 He said, "Don't Do It!"
1640164 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:43 O'Reilly's Books
1640121 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:34 `pwd`
1640243 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:58 a \' \` \$ b \" c
1640259 drwxr-xr-x 2 jleffler staff 68 Aug 25 13:01 a \\' \\\` \\$ b \\\" c
1640056 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:21 a b c
1640057 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:21 d e f
1640231 -rw-r--r-- 1 jleffler staff 223 Aug 25 12:56 x.sh
$
ls -ail
更改为后ls -dil
,输出为:
$ bash x.sh
arg: ! % *
arg: $(pwd)
arg: $PATH
arg: ${HOME}
arg: ' ` "
arg: He said, "Don't Do It!"
arg: O'Reilly's Books
arg: `pwd`
arg: a \' \` \$ b \" c
arg: a \\' \\\` \\$ b \\\" c
arg: a b c
arg: d e f
arg: x.sh
cmd: ls -dil '! % *' '$(pwd)' '$PATH' '${HOME}' ''\'' ` "' 'He said, "Don'\''t Do It!"' 'O'\''Reilly'\''s Books' '`pwd`' 'a \'\'' \` \$ b \" c' 'a \\'\'' \\\` \\$ b \\\" c' 'a b c' 'd e f' 'x.sh'
1640119 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:34 ! % *
1640120 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:34 $(pwd)
1640176 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:45 $PATH
1640175 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:45 ${HOME}
1640163 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:43 ' ` "
1640177 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:45 He said, "Don't Do It!"
1640164 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:43 O'Reilly's Books
1640121 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:34 `pwd`
1640243 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:58 a \' \` \$ b \" c
1640259 drwxr-xr-x 2 jleffler staff 68 Aug 25 13:01 a \\' \\\` \\$ b \\\" c
1640056 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:21 a b c
1640057 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:21 d e f
1640271 -rw-r--r-- 1 jleffler staff 223 Aug 25 13:03 x.sh
$
工作脚本
set -- *
cmd="ls -ail"
for arg in "$@"
do echo "arg: $arg"
escaped=$(sed -e "s/'/'\\\\''/g" -e "s/^/'/" -e "s/$/'/" <<< "$arg")
cmd="$cmd $escaped"
done
echo "cmd: $cmd"
tcsh -c "$cmd"
概括
解决方案的关键部分是:
- 认识到参数周围需要单引号。
- 知道如何转义单引号。
- 知道如何逃避反斜杠。
- 当你做你的测试时真的很残酷!
- 如果你以前做过,它会有所帮助...
哦,笨蛋!我忘了测试包含换行符的参数:
$ mkdir "a
> b
> c"
$ ls -li
total 8
1640119 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:34 ! % *
1640120 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:34 $(pwd)
1640176 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:45 $PATH
1640175 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:45 ${HOME}
1640163 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:43 ' ` "
1640177 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:45 He said, "Don't Do It!"
1640164 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:43 O'Reilly's Books
1640121 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:34 `pwd`
1640336 drwxr-xr-x 2 jleffler staff 68 Aug 25 13:16 a?b?c
1640243 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:58 a \' \` \$ b \" c
1640259 drwxr-xr-x 2 jleffler staff 68 Aug 25 13:01 a \\' \\\` \\$ b \\\" c
1640056 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:21 a b c
1640057 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:21 d e f
1640271 -rw-r--r-- 1 jleffler staff 223 Aug 25 13:03 x.sh
$
好吧,有一些原因为什么你不应该尝试解析来自ls
;的输出。它生成了问号来代替换行符(这是在 Mac OS X 10.8.1 上,不是GNU ls
,仅适用于那些在家记分的人;其他系统的行为可能会有所不同)。
当脚本 ( x.sh
) 运行时,我得到:
$ bash x.sh
arg: ! % *
arg: $(pwd)
arg: $PATH
arg: ${HOME}
arg: ' ` "
arg: He said, "Don't Do It!"
arg: O'Reilly's Books
arg: `pwd`
arg: a
b
c
arg: a \' \` \$ b \" c
arg: a \\' \\\` \\$ b \\\" c
arg: a b c
arg: d e f
arg: x.sh
cmd: ls -dil '! % *' '$(pwd)' '$PATH' '${HOME}' ''\'' ` "' 'He said, "Don'\''t Do It!"' 'O'\''Reilly'\''s Books' '`pwd`' 'a'
'b'
'c' 'a \'\'' \` \$ b \" c' 'a \\'\'' \\\` \\$ b \\\" c' 'a b c' 'd e f' 'x.sh'
ls: a: No such file or directory
1640119 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:34 ! % *
1640120 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:34 $(pwd)
1640176 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:45 $PATH
1640175 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:45 ${HOME}
1640163 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:43 ' ` "
1640177 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:45 He said, "Don't Do It!"
1640164 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:43 O'Reilly's Books
1640121 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:34 `pwd`
b: Command not found.
c: Command not found.
$
这里有多个问题。该sed
脚本分别处理参数的每一行。这真的不能用sed
; 或者,也许更准确地说,这不是我想用sed
. 很久以前,我编写了一个 C 程序escape
来完成sed
脚本几乎可以完成的工作。
#!/bin/bash
set -- *
escaped=$(escape "$@")
cmd="ls -dil $escaped"
echo "cmd: $cmd"
bash -c "$cmd"
tcsh -c "$cmd"
请注意,我已在bash
其中添加了对 in 的调用。输出是:
cmd: ls -dil '! % *' '$(pwd)' '$PATH' '${HOME}' ''\'' ` "' 'He said, "Don'\''t Do It!"' 'O'\''Reilly'\''s Books' '`pwd`' 'a
b
c' 'a b c' 'd e f' x.sh
178474064 drwxr-xr-x 2 jleffler staff 68 Aug 25 13:38 ! % *
178474065 drwxr-xr-x 2 jleffler staff 68 Aug 25 13:38 $(pwd)
178474219 drwxr-xr-x 2 jleffler staff 68 Aug 25 13:38 $PATH
178474218 drwxr-xr-x 2 jleffler staff 68 Aug 25 13:38 ${HOME}
178474170 drwxr-xr-x 2 jleffler staff 68 Aug 25 13:38 ' ` "
178474220 drwxr-xr-x 2 jleffler staff 68 Aug 25 13:38 He said, "Don't Do It!"
178474131 drwxr-xr-x 2 jleffler staff 68 Aug 25 13:38 O'Reilly's Books
178474066 drwxr-xr-x 2 jleffler staff 68 Aug 25 13:38 `pwd`
178474998 drwxr-xr-x 2 jleffler staff 68 Aug 25 13:40 a?b?c
178473958 drwxr-xr-x 2 jleffler staff 68 Aug 25 13:38 a b c
178473959 drwxr-xr-x 2 jleffler staff 68 Aug 25 13:38 d e f
178475097 -rw-r--r-- 1 jleffler staff 115 Aug 25 13:41 x.sh
Unmatched '.
b: Command not found.
Unmatched '.
废话?好吧,bash
从 Bourne shell 派生的其他 shell,例如ksh
, 可以使用从一行开始并在其他行上继续的字符串,但 C shell 及其派生类则不行。他们要求在换行符之前使用反斜杠。因此,要使用tcsh
,我必须升级escape
以生成 C shell 的输出。一点也不难做,但需要做。据推测,这将是一种选择-c
,并且为了通用安全,调用将变为:
escaped=$(escape -c -- "$@")
使用双破折号防止将参数误解"$@"
为自身的选项escape
。在某种程度上,这表明很难编写脚本来处理包含可移植文件名字符集之外的字符的文件名。幸运的是,我不必经常处理 C shell;我不打算将其作为其中的一部分,escape
因为它是接口的更改(当前代码没有自己的任何选项,因此我不使用双破折号表示法escape
)。如果我需要它,它将成为cescape
无条件支持C shell。