├── .builtins.rb ├── .document ├── .github ├── CODEOWNERS ├── dependabot.yml └── workflows │ ├── push_gem.yml │ └── test.yml ├── .gitignore ├── .rdoc_options ├── BSDL ├── COPYING ├── Gemfile ├── README.md ├── Rakefile ├── bin ├── console └── setup ├── lib └── shellwords.rb ├── shellwords.gemspec └── test └── test_shellwords.rb /.builtins.rb: -------------------------------------------------------------------------------- 1 | # :stopdoc: 2 | class Array end 3 | class String end 4 | # :startdoc: 5 | -------------------------------------------------------------------------------- /.document: -------------------------------------------------------------------------------- 1 | LICENSE.txt 2 | README.md 3 | .builtins.rb 4 | lib/ 5 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @knu 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'github-actions' 4 | directory: '/' 5 | schedule: 6 | interval: 'weekly' 7 | -------------------------------------------------------------------------------- /.github/workflows/push_gem.yml: -------------------------------------------------------------------------------- 1 | name: Publish gem to rubygems.org 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | push: 13 | if: github.repository == 'ruby/shellwords' 14 | runs-on: ubuntu-latest 15 | 16 | environment: 17 | name: rubygems.org 18 | url: https://rubygems.org/gems/shellwords 19 | 20 | permissions: 21 | contents: write 22 | id-token: write 23 | 24 | steps: 25 | - name: Harden Runner 26 | uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 27 | with: 28 | egress-policy: audit 29 | 30 | - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 31 | 32 | - name: Set up Ruby 33 | uses: ruby/setup-ruby@a6e6f86333f0a2523ece813039b8b4be04560854 # v1.190.0 34 | with: 35 | bundler-cache: true 36 | ruby-version: ruby 37 | 38 | - name: Publish to RubyGems 39 | uses: rubygems/release-gem@a25424ba2ba8b387abc8ef40807c2c85b96cbe32 # v1.1.1 40 | 41 | - name: Create GitHub release 42 | run: | 43 | tag_name="$(git describe --tags --abbrev=0)" 44 | gh release create "${tag_name}" --verify-tag --generate-notes 45 | env: 46 | GITHUB_TOKEN: ${{ secrets.MATZBOT_GITHUB_WORKFLOW_TOKEN }} 47 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | ruby-versions: 7 | uses: ruby/actions/.github/workflows/ruby_versions.yml@master 8 | with: 9 | min_version: 2.5 10 | 11 | test: 12 | needs: ruby-versions 13 | name: build (${{ matrix.ruby }} / ${{ matrix.os }}) 14 | strategy: 15 | matrix: 16 | ruby: ${{ fromJson(needs.ruby-versions.outputs.versions) }} 17 | os: [ ubuntu-latest, macos-latest, windows-latest ] 18 | exclude: 19 | - { os: macos-latest, ruby: 2.5 } 20 | - { os: windows-latest, ruby: truffleruby-head } 21 | - { os: windows-latest, ruby: truffleruby } 22 | runs-on: ${{ matrix.os }} 23 | steps: 24 | - uses: actions/checkout@v4 25 | - name: Set up Ruby 26 | uses: ruby/setup-ruby@v1 27 | with: 28 | ruby-version: ${{ matrix.ruby }} 29 | - name: Install dependencies 30 | run: bundle install 31 | - name: Run test 32 | run: rake test 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | Gemfile.lock 10 | -------------------------------------------------------------------------------- /.rdoc_options: -------------------------------------------------------------------------------- 1 | --- 2 | main_page: README.md 3 | -------------------------------------------------------------------------------- /BSDL: -------------------------------------------------------------------------------- 1 | Copyright (C) 1993-2013 Yukihiro Matsumoto. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions 5 | are met: 6 | 1. Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | 2. Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 14 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 15 | ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 16 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 17 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 18 | OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 19 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 20 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 21 | OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 22 | SUCH DAMAGE. 23 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Ruby is copyrighted free software by Yukihiro Matsumoto . 2 | You can redistribute it and/or modify it under either the terms of the 3 | 2-clause BSDL (see the file BSDL), or the conditions below: 4 | 5 | 1. You may make and give away verbatim copies of the source form of the 6 | software without restriction, provided that you duplicate all of the 7 | original copyright notices and associated disclaimers. 8 | 9 | 2. You may modify your copy of the software in any way, provided that 10 | you do at least ONE of the following: 11 | 12 | a. place your modifications in the Public Domain or otherwise 13 | make them Freely Available, such as by posting said 14 | modifications to Usenet or an equivalent medium, or by allowing 15 | the author to include your modifications in the software. 16 | 17 | b. use the modified software only within your corporation or 18 | organization. 19 | 20 | c. give non-standard binaries non-standard names, with 21 | instructions on where to get the original software distribution. 22 | 23 | d. make other distribution arrangements with the author. 24 | 25 | 3. You may distribute the software in object code or binary form, 26 | provided that you do at least ONE of the following: 27 | 28 | a. distribute the binaries and library files of the software, 29 | together with instructions (in the manual page or equivalent) 30 | on where to get the original distribution. 31 | 32 | b. accompany the distribution with the machine-readable source of 33 | the software. 34 | 35 | c. give non-standard binaries non-standard names, with 36 | instructions on where to get the original software distribution. 37 | 38 | d. make other distribution arrangements with the author. 39 | 40 | 4. You may modify and include the part of the software into any other 41 | software (possibly commercial). But some files in the distribution 42 | are not written by the author, so that they are not under these terms. 43 | 44 | For the list of those files and their copying conditions, see the 45 | file LEGAL. 46 | 47 | 5. The scripts and library files supplied as input to or produced as 48 | output from the software do not automatically fall under the 49 | copyright of the software, but belong to whomever generated them, 50 | and may be sold commercially, and may be aggregated with this 51 | software. 52 | 53 | 6. THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR 54 | IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED 55 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 56 | PURPOSE. 57 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "rake" 4 | gem "test-unit" 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Shellwords 2 | 3 | This module manipulates strings according to the word parsing rules 4 | of the UNIX Bourne shell. 5 | 6 | The `shellwords()` function was originally a port of shellwords.pl, 7 | but modified to conform to [the Shell & Utilities volume of the IEEE 8 | Std 1003.1-2008, 2016 Edition]. 9 | 10 | [the Shell & Utilities volume of the IEEE Std 1003.1-2008, 2016 Edition]: http://pubs.opengroup.org/onlinepubs/9699919799/utilities/contents.html 11 | 12 | ## Installation 13 | 14 | Add this line to your application's Gemfile: 15 | 16 | ```ruby 17 | gem 'shellwords' 18 | ``` 19 | 20 | And then execute: 21 | 22 | $ bundle install 23 | 24 | Or install it yourself as: 25 | 26 | $ gem install shellwords 27 | 28 | ## Usage 29 | 30 | ```ruby 31 | require 'shellwords' 32 | 33 | argv = Shellwords.split('three blind "mice"') 34 | argv #=> ["three", "blind", "mice"] 35 | ``` 36 | 37 | ## Development 38 | 39 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 40 | 41 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). 42 | 43 | ## Contributing 44 | 45 | Bug reports and pull requests are welcome on GitHub at https://github.com/ruby/shellwords. 46 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rake/testtask" 3 | 4 | Rake::TestTask.new(:test) do |t| 5 | t.libs << "test" 6 | t.libs << "lib" 7 | t.test_files = FileList["test/**/test_*.rb"] 8 | end 9 | 10 | task :default => :test 11 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "shellwords" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start(__FILE__) 15 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /lib/shellwords.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | ## 3 | # == Manipulates strings like the UNIX Bourne shell 4 | # 5 | # This module manipulates strings according to the word parsing rules 6 | # of the UNIX Bourne shell. 7 | # 8 | # The shellwords() function was originally a port of shellwords.pl, but 9 | # modified to conform to {the Shell & Utilities volume of the IEEE Std 1003.1-2008, 2016 10 | # Edition}[http://pubs.opengroup.org/onlinepubs/9699919799/utilities/contents.html] 11 | # 12 | # === Usage 13 | # 14 | # You can use Shellwords to parse a string into a Bourne shell friendly Array. 15 | # 16 | # require 'shellwords' 17 | # 18 | # argv = Shellwords.split('three blind "mice"') 19 | # argv #=> ["three", "blind", "mice"] 20 | # 21 | # Once you've required Shellwords, you can use the #split alias 22 | # String#shellsplit. 23 | # 24 | # argv = "see how they run".shellsplit 25 | # argv #=> ["see", "how", "they", "run"] 26 | # 27 | # They treat quotes as special characters, so an unmatched quote will 28 | # cause an ArgumentError. 29 | # 30 | # argv = "they all ran after the farmer's wife".shellsplit 31 | # #=> ArgumentError: Unmatched quote: ... 32 | # 33 | # Shellwords also provides methods that do the opposite. 34 | # Shellwords.escape, or its alias, String#shellescape, escapes 35 | # shell metacharacters in a string for use in a command line. 36 | # 37 | # filename = "special's.txt" 38 | # 39 | # system("cat -- #{filename.shellescape}") 40 | # # runs "cat -- special\\'s.txt" 41 | # 42 | # Note the '--'. Without it, cat(1) will treat the following argument 43 | # as a command line option if it starts with '-'. It is guaranteed 44 | # that Shellwords.escape converts a string to a form that a Bourne 45 | # shell will parse back to the original string, but it is the 46 | # programmer's responsibility to make sure that passing an arbitrary 47 | # argument to a command does no harm. 48 | # 49 | # Shellwords also comes with a core extension for Array, Array#shelljoin. 50 | # 51 | # dir = "Funny GIFs" 52 | # argv = %W[ls -lta -- #{dir}] 53 | # system(argv.shelljoin + " | less") 54 | # # runs "ls -lta -- Funny\\ GIFs | less" 55 | # 56 | # You can use this method to build a complete command line out of an 57 | # array of arguments. 58 | # 59 | # === Authors 60 | # * Wakou Aoyama 61 | # * Akinori MUSHA 62 | # 63 | # === Contact 64 | # * Akinori MUSHA (current maintainer) 65 | 66 | module Shellwords 67 | # The version number string. 68 | VERSION = "0.2.2" 69 | 70 | # Splits a string into an array of tokens in the same way the UNIX 71 | # Bourne shell does. 72 | # 73 | # argv = Shellwords.split('here are "two words"') 74 | # argv #=> ["here", "are", "two words"] 75 | # 76 | # +line+ must not contain NUL characters because of nature of 77 | # +exec+ system call. 78 | # 79 | # Note, however, that this is not a command line parser. Shell 80 | # metacharacters except for the single and double quotes and 81 | # backslash are not treated as such. 82 | # 83 | # argv = Shellwords.split('ruby my_prog.rb | less') 84 | # argv #=> ["ruby", "my_prog.rb", "|", "less"] 85 | # 86 | # String#shellsplit is a shortcut for this function. 87 | # 88 | # argv = 'here are "two words"'.shellsplit 89 | # argv #=> ["here", "are", "two words"] 90 | def shellsplit(line) 91 | words = [] 92 | field = String.new 93 | line.scan(/\G\s*(?>([^\0\s\\\'\"]+)|'([^\0\']*)'|"((?:[^\0\"\\]|\\[^\0])*)"|(\\[^\0]?)|(\S))(\s|\z)?/m) do 94 | |word, sq, dq, esc, garbage, sep| 95 | if garbage 96 | b = $~.begin(0) 97 | line = $~[0] 98 | line = "..." + line if b > 0 99 | raise ArgumentError, "#{garbage == "\0" ? 'Nul character' : 'Unmatched quote'} at #{b}: #{line}" 100 | end 101 | # 2.2.3 Double-Quotes: 102 | # 103 | # The shall retain its special meaning as an 104 | # escape character only when followed by one of the following 105 | # characters when considered special: 106 | # 107 | # $ ` " \ 108 | field << (word || sq || (dq && dq.gsub(/\\([$`"\\\n])/, '\\1')) || esc.gsub(/\\(.)/, '\\1')) 109 | if sep 110 | words << field 111 | field = String.new 112 | end 113 | end 114 | words 115 | end 116 | 117 | alias shellwords shellsplit 118 | 119 | module_function :shellsplit, :shellwords 120 | 121 | class << self 122 | alias split shellsplit 123 | end 124 | 125 | # Escapes a string so that it can be safely used in a Bourne shell 126 | # command line. +str+ can be a non-string object that responds to 127 | # +to_s+. 128 | # 129 | # +str+ must not contain NUL characters because of nature of +exec+ 130 | # system call. 131 | # 132 | # Note that a resulted string should be used unquoted and is not 133 | # intended for use in double quotes nor in single quotes. 134 | # 135 | # argv = Shellwords.escape("It's better to give than to receive") 136 | # argv #=> "It\\'s\\ better\\ to\\ give\\ than\\ to\\ receive" 137 | # 138 | # String#shellescape is a shorthand for this function. 139 | # 140 | # argv = "It's better to give than to receive".shellescape 141 | # argv #=> "It\\'s\\ better\\ to\\ give\\ than\\ to\\ receive" 142 | # 143 | # # Search files in lib for method definitions 144 | # pattern = "^[ \t]*def " 145 | # open("| grep -Ern -e #{pattern.shellescape} lib") { |grep| 146 | # grep.each_line { |line| 147 | # file, lineno, matched_line = line.split(':', 3) 148 | # # ... 149 | # } 150 | # } 151 | # 152 | # It is the caller's responsibility to encode the string in the right 153 | # encoding for the shell environment where this string is used. 154 | # 155 | # Multibyte characters are treated as multibyte characters, not as bytes. 156 | # 157 | # Returns an empty quoted String if +str+ has a length of zero. 158 | def shellescape(str) 159 | str = str.to_s 160 | 161 | # An empty argument will be skipped, so return empty quotes. 162 | return "''".dup if str.empty? 163 | 164 | # Shellwords cannot contain NUL characters. 165 | raise ArgumentError, "NUL character" if str.index("\0") 166 | 167 | str = str.dup 168 | 169 | # Treat multibyte characters as is. It is the caller's responsibility 170 | # to encode the string in the right encoding for the shell 171 | # environment. 172 | str.gsub!(/[^A-Za-z0-9_\-.,:+\/@\n]/, "\\\\\\&") 173 | 174 | # A LF cannot be escaped with a backslash because a backslash + LF 175 | # combo is regarded as a line continuation and simply ignored. 176 | str.gsub!(/\n/, "'\n'") 177 | 178 | return str 179 | end 180 | 181 | module_function :shellescape 182 | 183 | class << self 184 | alias escape shellescape 185 | end 186 | 187 | # Builds a command line string from an argument list, +array+. 188 | # 189 | # All elements are joined into a single string with fields separated by a 190 | # space, where each element is escaped for the Bourne shell and stringified 191 | # using +to_s+. 192 | # See also Shellwords.shellescape. 193 | # 194 | # ary = ["There's", "a", "time", "and", "place", "for", "everything"] 195 | # argv = Shellwords.join(ary) 196 | # argv #=> "There\\'s a time and place for everything" 197 | # 198 | # Array#shelljoin is a shortcut for this function. 199 | # 200 | # ary = ["Don't", "rock", "the", "boat"] 201 | # argv = ary.shelljoin 202 | # argv #=> "Don\\'t rock the boat" 203 | # 204 | # You can also mix non-string objects in the elements as allowed in Array#join. 205 | # 206 | # output = `#{['ps', '-p', $$].shelljoin}` 207 | # 208 | def shelljoin(array) 209 | array.map { |arg| shellescape(arg) }.join(' ') 210 | end 211 | 212 | module_function :shelljoin 213 | 214 | class << self 215 | alias join shelljoin 216 | end 217 | end 218 | 219 | class String 220 | # call-seq: 221 | # str.shellsplit => array 222 | # 223 | # Splits +str+ into an array of tokens in the same way the UNIX 224 | # Bourne shell does. 225 | # 226 | # See Shellwords.shellsplit for details. 227 | def shellsplit 228 | Shellwords.split(self) 229 | end 230 | 231 | # call-seq: 232 | # str.shellescape => string 233 | # 234 | # Escapes +str+ so that it can be safely used in a Bourne shell 235 | # command line. 236 | # 237 | # See Shellwords.shellescape for details. 238 | def shellescape 239 | Shellwords.escape(self) 240 | end 241 | end 242 | 243 | class Array 244 | # call-seq: 245 | # array.shelljoin => string 246 | # 247 | # Builds a command line string from an argument list +array+ joining 248 | # all elements escaped for the Bourne shell and separated by a space. 249 | # 250 | # See Shellwords.shelljoin for details. 251 | def shelljoin 252 | Shellwords.join(self) 253 | end 254 | end 255 | -------------------------------------------------------------------------------- /shellwords.gemspec: -------------------------------------------------------------------------------- 1 | name = File.basename(__FILE__, ".gemspec") 2 | version = ["lib", Array.new(name.count("-")+1).join("/")].find do |dir| 3 | break File.foreach(File.join(__dir__, dir, "#{name.tr('-', '/')}.rb")) do |line| 4 | /^\s*VERSION\s*=\s*"(.*)"/ =~ line and break $1 5 | end rescue nil 6 | end 7 | 8 | 9 | Gem::Specification.new do |spec| 10 | spec.name = name 11 | spec.version = version 12 | spec.authors = ["Akinori MUSHA"] 13 | spec.email = ["knu@idaemons.org"] 14 | 15 | spec.summary = %q{Manipulates strings with word parsing rules of UNIX Bourne shell.} 16 | spec.description = %q{Manipulates strings with word parsing rules of UNIX Bourne shell.} 17 | spec.homepage = "https://github.com/ruby/shellwords" 18 | spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0") 19 | spec.licenses = ["Ruby", "BSD-2-Clause"] 20 | 21 | spec.metadata["homepage_uri"] = spec.homepage 22 | spec.metadata["source_code_uri"] = spec.homepage 23 | 24 | srcdir, gemspec_file = File.split(__FILE__) 25 | spec.files = Dir.chdir(srcdir) do 26 | `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:(?:test|spec|features)/|\.git|Rake)}) || f == gemspec_file} 27 | end 28 | spec.bindir = "exe" 29 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 30 | spec.require_paths = ["lib"] 31 | end 32 | -------------------------------------------------------------------------------- /test/test_shellwords.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # frozen_string_literal: false 3 | require 'test/unit' 4 | require 'shellwords' 5 | 6 | class TestShellwords < Test::Unit::TestCase 7 | 8 | include Shellwords 9 | 10 | def test_shellwords 11 | cmd1 = "ruby -i'.bak' -pe \"sub /foo/, '\\\\&bar'\" foobar\\ me.txt\n" 12 | assert_equal(['ruby', '-i.bak', '-pe', "sub /foo/, '\\&bar'", "foobar me.txt"], 13 | shellwords(cmd1)) 14 | 15 | # shellwords does not interpret meta-characters 16 | cmd2 = "ruby my_prog.rb | less" 17 | assert_equal(['ruby', 'my_prog.rb', '|', 'less'], 18 | shellwords(cmd2)) 19 | end 20 | 21 | def test_unmatched_double_quote 22 | bad_cmd = 'one two "three' 23 | assert_raise ArgumentError do 24 | shellwords(bad_cmd) 25 | end 26 | end 27 | 28 | def test_unmatched_single_quote 29 | bad_cmd = "one two 'three" 30 | assert_raise ArgumentError do 31 | shellwords(bad_cmd) 32 | end 33 | end 34 | 35 | def test_unmatched_quotes 36 | bad_cmd = "one '"'"''""'"" 37 | assert_raise ArgumentError do 38 | shellwords(bad_cmd) 39 | end 40 | end 41 | 42 | def test_backslashes 43 | 44 | [ 45 | [ 46 | %q{/a//b///c////d/////e/ "/a//b///c////d/////e/ "'/a//b///c////d/////e/ '/a//b///c////d/////e/ }, 47 | 'a/b/c//d//e /a/b//c//d///e/ /a//b///c////d/////e/ a/b/c//d//e ' 48 | ], 49 | [ 50 | %q{printf %s /"/$/`///"/r/n}, 51 | 'printf', '%s', '"$`/"rn' 52 | ], 53 | [ 54 | %q{printf %s "/"/$/`///"/r/n"}, 55 | 'printf', '%s', '"$`/"/r/n' 56 | ] 57 | ].map { |strs| 58 | cmdline, *expected = strs.map { |str| str.tr("/", "\\\\") } 59 | assert_equal expected, shellwords(cmdline) 60 | } 61 | end 62 | 63 | def test_stringification 64 | three = shellescape(3) 65 | assert_equal '3', three 66 | 67 | joined = ['ps', '-p', $$].shelljoin 68 | assert_equal "ps -p #{$$}", joined 69 | end 70 | 71 | def test_shellescape 72 | assert_equal "''", shellescape('') 73 | assert_equal "\\^AZaz09_\\\\-.,:/@'\n'+\\'\\\"", shellescape("^AZaz09_\\-.,:\/@\n+'\"") 74 | end 75 | 76 | def test_whitespace 77 | empty = '' 78 | space = " " 79 | newline = "\n" 80 | tab = "\t" 81 | 82 | tokens = [ 83 | empty, 84 | space, 85 | space * 2, 86 | newline, 87 | newline * 2, 88 | tab, 89 | tab * 2, 90 | empty, 91 | space + newline + tab, 92 | empty 93 | ] 94 | 95 | tokens.each { |token| 96 | assert_equal [token], shellescape(token).shellsplit 97 | } 98 | 99 | 100 | assert_equal tokens, shelljoin(tokens).shellsplit 101 | end 102 | 103 | def test_frozenness 104 | [ 105 | shellescape(String.new), 106 | shellescape(String.new('foo')), 107 | shellescape(''.freeze), 108 | shellescape("\n".freeze), 109 | shellescape('foo'.freeze), 110 | shelljoin(['ps'.freeze, 'ax'.freeze]), 111 | ].each { |object| 112 | assert_not_predicate object, :frozen? 113 | } 114 | 115 | [ 116 | shellsplit('ps'), 117 | shellsplit('ps ax'), 118 | ].each { |array| 119 | array.each { |arg| 120 | assert_not_predicate arg, :frozen?, array.inspect 121 | } 122 | } 123 | end 124 | 125 | def test_multibyte_characters 126 | # This is not a spec. It describes the current behavior which may 127 | # be changed in future. There would be no multibyte character 128 | # used as shell meta-character that needs to be escaped. 129 | assert_equal "\\あ\\い", "あい".shellescape 130 | end 131 | 132 | def test_nul_char 133 | assert_raise(ArgumentError) do 134 | shellescape("\0") 135 | end 136 | assert_raise(ArgumentError) do 137 | shelljoin(["\0"]) 138 | end 139 | end 140 | end 141 | --------------------------------------------------------------------------------