[ruby-dev:31768] shellescape

From: "Akinori MUSHA" <knu@...>
Date: 2007-09-10 06:10:13 UTC
List: ruby-dev #31768
 shellwords.rb に shellescape() 等を追加しようと思います。

 理由としては、 system(), open("|..."), IO.popen() 等でコマンド
ラインを記述する際、メタ文字等をエスケープする適当な関数が標準で
提供されていないためです。

 仕様としては、Bourneシェルに渡すことを想定し、安全でない文字を
バックスラッシュ(\)でエスケープします。文字列はシングルバイト列
として扱います。理由は以下の通りです。

- shellwords はそもそもBourneシェル(POSIX)の文法に則った処理を行う
  関数を提供するライブラリである。

- (Plan9の) rc 等を含め多くのシェルで使えるのが望ましいという声も
  [ruby-list:30049] 等で出た。しかし、Bourneシェルと rc では引用符
  (')のエスケープの仕方に互換性がなく、 Windows の cmd.exe も変数
  展開や引用符の扱いの差異が大きく、ひとつで全対応するエスケープ
  方法がない。

  ただし、 (t)csh は改行(LF)のエスケープが効かない(そもそもできる
  のか不明)点を除けば互換性がある。

- 引用符(')で括る方式だと、感嘆符(!)の扱いの差異のため (t)csh で
  使えなくなってしまう。また、マルチバイト文字の中に引用符(')が
  あった場合、マルチバイト対応のシェルで '...'\''...' が誤解釈
  される可能性が高い。

- 二重引用符(")で括る方式だと、バックスラッシュ(\)の扱いの差異の
  ため (t)csh で使えなくなってしまう。


 パッチを添付しました。以下がサマリーです。

- Shellwords.shellescape を追加:
    Bourneシェルに解釈される可能性のある文字をエスケープして返す
    (仕様は実装およびコメントを参照のこと)

- Shellwords.shelljoin を追加:
    与えられた引数列から、Bourneシェルコマンドライン文字列を生成
    して返す (shellescape を利用)

- Shellwords.shellwords の別名 shellsplit を定義

- 利用の便のため String や Array にインスタンスメソッドを追加


 cmd.exe や rc の対応については、将来的にオプション引数を追加し、
プラットフォームごとに妥当なデフォルト値を設定することで対応でき
ればと思います。

--
                     /
                    /__  __            Akinori.org / MUSHA.org
                   / )  )  ) )  /     FreeBSD.org / Ruby-lang.org
Akinori MUSHA aka / (_ /  ( (__(  @ iDaemons.org / and.or.jp

"Different eyes see different things,
    Different hearts beat on different strings --
       But there are times for you and me when all such things agree"

Index: shellwords.rb
===================================================================
--- shellwords.rb	(revision 13420)
+++ shellwords.rb	(working copy)
@@ -1,29 +1,31 @@
 #
-# shellwords.rb: Split text into an array of tokens a la UNIX shell
+# shellwords.rb: Manipulates strings a la UNIX Bourne shell
 #

 #
-# This module is originally a port of shellwords.pl, but modified to
-# conform to POSIX / SUSv3 (IEEE Std 1003.1-2001).
+# This module manipulates strings according to the word parsing rules
+# of the UNIX Bourne shell.
 #
-# Examples:
+# The shellwords() function was originally a port of shellwords.pl,
+# but modified to conform to POSIX / SUSv3 (IEEE Std 1003.1-2001).
 #
-#   require 'shellwords'
-#   words = Shellwords.shellwords(line)
-#
-# or
-#
-#   require 'shellwords'
-#   include Shellwords
-#   words = shellwords(line)
+# Authors:
+#   - Wakou Aoyama
+#   - Akinori MUSHA <knu@iDaemons.org>
 #
 module Shellwords

   #
-  # Split text into an array of tokens in the same way the UNIX Bourne
-  # shell does.
+  # Splits a string into an array of tokens in the same way the UNIX
+  # Bourne shell does.
+  #
+  #   argv = Shellwords.shellwords('here are "two words"')
+  #   argv #=> ["here", "are", "two words"]
   #
-  # See the +Shellwords+ module documentation for an example.
+  # +String#shellwords+ is a shorthand for this function.
+  #
+  #   argv = 'here are "two words"'.shellwords
+  #   argv #=> ["here", "are", "two words"]
   #
   def shellwords(line)
     words = []
@@ -39,6 +41,98 @@
     end
     words
   end
+  alias shellsplit shellwords
+
+  #
+  # Escapes a string so that it can be safely used in a Bourne shell
+  # command line.
+  #
+  # Note that a resulted string should be used unquoted and is not
+  # intended for use in double quotes nor in single quotes.
+  #
+  #   open("| grep #{Shellwords.shellescape(pattern)} file") { |pipe|
+  #     # ...
+  #   }
+  #
+  # +String#shellescape+ is a shorthand for this function.
+  #
+  #   open("| grep #{pattern.shellescape} file") { |pipe|
+  #     # ...
+  #   }
+  #
+  def shellescape(str)
+    # An empty argument will be skipped, so return empty quotes.
+    return "''" if str.empty?
+
+    str = str.dup
+
+    # Process as a single byte sequence because not all shell
+    # implementations are multibyte aware.
+    str.gsub!(/([^A-Za-z0-9_\-.,:\/@\n])/n, "\\\\\\1")
+
+    # A LF cannot be escaped with a backslash because a backslash + LF
+    # combo is regarded as line continuation and simply ignored.
+    str.gsub!(/\n/, "'\n'")
+
+    return str
+  end
+
+  #
+  # Builds a command line string from an argument list +array+ joining
+  # all elements escaped for Bourne shell and separated by a space.
+  #
+  #   open('|' + Shellwords.shelljoin(['grep', pattern, *files])) { |pipe|
+  #     # ...
+  #   }
+  #
+  # +Array#shelljoin+ is a shorthand for this function.
+  #
+  #   open('|' + ['grep', pattern, *files].shelljoin) { |pipe|
+  #     # ...
+  #   }
+  #
+  def shelljoin(array)
+    array.map { |arg| shellescape(arg) }.join(' ')
+  end
+
+  module_function :shellwords, :shellsplit, :shellescape, :shelljoin
+end
+
+class String
+  #
+  # call-seq:
+  #   str.shellwords => array
+  #
+  # Splits +str+ into an array of tokens in the same way the UNIX
+  # Bourne shell does.  See +Shellwords::shellwords+ for details.
+  #
+  def shellwords
+    Shellwords.shellwords(self)
+  end
+  alias shellsplit shellwords

-  module_function :shellwords
+  #
+  # call-seq:
+  #   str.shellescape => string
+  #
+  # Escapes +str+ so that it can be safely used in a Bourne shell
+  # command line.  See +Shellwords::shellescape+ for details.
+  #
+  def shellescape
+    Shellwords.shellescape(self)
+  end
+end
+
+class Array
+  #
+  # call-seq:
+  #   array.shelljoin => string
+  #
+  # Builds a command line string from an argument list +array+ joining
+  # all elements escaped for Bourne shell and separated by a space.
+  # See +Shellwords::shelljoin+ for details.
+  #
+  def shelljoin
+    Shellwords.shelljoin(self)
+  end
 end

In This Thread

Prev Next