Parse multiple command line options in Ruby using OptionParser - ruby

I've just started using OptionParser for Ruby and I wanted to use flags that would use more than just one argument.
For instance, I would like to be able to run:
script --move src dst
Note how src and dst are not separated using a coma.
My initial idea was:
opts.on("-m src dst", "--move src dst ", "move file from SRCto DST") do |src|
# do something
end
But this is not working. I assume that this is not the right approach. But how could this be done?

The example under the "Complete Example" section of the OptionParser details how a list of items can be accepted.
Here is a sample program based on that example. The third parameter Array in opts.on indicates that input src, dst should be used to create an array. To run this sample, you need to do gem install trollop.
# test.rb
require 'optparse'
options = {}
OptionParser.new do |opt|
opt.on("-m src, dst", "--move src, dst", Array, "Move from src to dst") do |list|
options[:src] = list[0]
options[:dst] = list[1]
end
end.parse!
puts options # It's a hash of parsed options
Sample run:
> ruby test.rb -m from,to
{:src=>"src", :dst=>"dst"}
>ruby test.rb -h
Usage: test [options]
-m, --move src, dst Move from src to dst
The above script forces one to separate the options using comma.
As indicated by "Really Cheap Command-Line Option Parsing in Ruby", there seems to be a gem, trollop, that can be quite easy to use for command-line parsing.
A sample program based on Trollop is given below, which allows usage of spaces for specifying options with multiple values
# test.rb
require "trollop"
opts = Trollop::options do
banner "Command line parsing using Trollop"
opt :move, "--move src dst', Move from src to dst", :short => "-m", :long => "--move", :type => :strings
end
# An array of option values
p opts.move
Sample run:
>ruby test.rb -m hello world
["hello", "world"]
>ruby test.rb -h
Command line parsing using Trollop
-m, --move=<s+> '--move src dst', Move from src to dst
-h, --help Show this message
There is a subtle difference between the help output between the two approaches. Trollop produces help text where --move=<s+> does not indicate clearly that it needs accepts two values, so I had to repeat the command syntax description.

OptionParser doesn't support that; It could be patched to do so, but I'm not sure it's worth the trouble.
Consider this code:
require 'optparse'
options = {}
OptionParser.new do |opt|
opt.on('-m', '--move') { |o| options[:move] = o }
end.parse!
from_name, to_name = ARGV
puts "Should move: #{ options.key?(:move) }"
puts "From: #{ from_name }"
puts "To: #{ to_name }"
Saving it and running it with various combinations of the parameters returns:
> ruby test.rb --move from to
Should move: true
From: from
To: to
> ruby test.rb from to
Should move: false
From:
To:
If the code is supposed to move files by default then don't bother with the --move flag, simply use:
test.rb from to
and consider removing the OptionParser block entirely.
If the code is supposed to normally copy with the option to move, then --move becomes more sensible to act as a flag that moving is desired.
ruby test.rb --move from to
I'd have code that tests for options[:move] and run the code to move instead of copy at that point.
In either case, the filenames shouldn't be tied to the flag, they should be supplied separately and retrieved from ARGV after OptionParser has finished parsing the command-line and removing entries it's handled.

Related

How to avoid generating content with quotes from an erb template?

I have written an erb template which I'm using to generate a Dockerfile dynamically. Here is how the whole thing works:
Take the input from command line.
Create an ruby array out of the input.
Use the Dockerfile.erb to generate Dockerfile based of this input.
The problem that I face here is that when the input is copied onto the generated Dockerfile, it has quotes around it which I need to avoid. Is there a way to do so?
Dockerfile.erb
FROM docker.cernerrepos.net/alpine:3.7 AS builder
LABEL bundles=<%= keys[:bundles] %>
generate.rb
require 'optparse'
require 'erb'
DEBUG_PREFIX = '>>> generate_dockerfile.rb ->'
#options = {}
#options[:output]= '.'
optparse = OptionParser.new do |opts|
opts.banner = 'Usage: generate_dockerfile.rb [options]'
opts.on('-b [BUNDLES]', '--bundles [BUNDLES]', "Comma separated list of bundles without extension and version.") do |bundles|
#options[:bundles] = bundles.split(',')
end
opts.on('-h', '--help', 'Display this Help') do
puts opts
exit
end
end
optparse.parse!
keys = #options
File.open('Dockerfile', 'w+') do |f|
f.write(ERB.new(File.read('Dockerfile.erb'), nil, '-').result(binding))
end
Actual Output Dockerfile
FROM docker.cernerrepos.net/alpine:3.7 AS builder
LABEL bundles=["sample_bundle1", "sample_bundle2"]
Expected Output Dockerfile
FROM docker.cernerrepos.net/alpine:3.7 AS builder
LABEL bundles=[sample_bundle1, sample_bundle2]
Command to Execute
ruby generate.rb -b sample_bundle1,sample_bundle2
Edit 1: Modified the question to include the minimum sample code, the expected out and the actual output and the command to run the code.
Right here:
LABEL bundles=<%= keys[:bundles] %>
You are basically calling keys[:bundle].to_s and interpolating that in the result.
What's ["foo", "bar"].to_s? It's ["foo", "bar"] (with quotes!)
That's why you're getting this result.
You can change it to the following, if you want to remove the quotations:
LABEL bundles=[<%= keys[:bundles].join(", ") %>]
Not to say this is the wrong approach, but isn't really a typical thing to do, so I don't think there's another built-in way to do this.

How to get command line argument using optparse?

I'm working on command line search tool which searches through source files using given keyword. I use optparse to parse command line options. For now it look like this:
qs -p ~/Desktop/project -k line
if no -p argument is provided I use default(current) directory:
qs -k line
But, what I really want is doing the same as above but without -k, like this:
qs line
For now I have this:
OptionParser.new do |options|
options.banner = "Usage: qs [options] [path] [keyword]\n" \
"\nSearch through files for a given keyword at specified path.\n" \
"If path is no specified current directory used by default."
options.separator ""
options.separator "Specific options:"
options.on("--no-rec", "Looking in files only at that level.") do |r|
$values[:recursion] = r
end
options.on('-p', '--path PATH', String, 'Path where to start search') do |path|
$values[:path] = path
end
options.on('-k', '--key WORD', String, 'Word you are looking for ') do |key|
$values[:keyword] = key
end
options.on_tail("-h" , "--help", "Help documentation.") do
$stderr.puts options
exit 1
end
end.parse!
As you can see it's impossible to do something like this:
$values[:keyword] = ARGV[2]
because there is no guarantee that all arguments will be present.
Is it possible to do this, without losing all this support from optparse ?
Thanks in advance.
When you use parse! (with the !), Optparse removes the options from ARGV, so afterward any other items (that Optparse doesn’t recognise) will be at ARGV[0] onwards.
So you should be able to do something like this:
OptionParser.new do |options|
# as before, but without k option
end.parse!
# At this point all the options optparse knows about will be
# removed from ARGV, so you can get at what's left:
$values[:keyword] = ARGV[0]

How do you use OptionParser to open a file with command-line options?

I'm getting this error when trying to print line from a file via command-line options
no implicit conversion of true into String (TypeError)
from threat_detection.rb:64:in 'new'
from threat_detection.rb:64:in '<main>'
If I use the file name instead of options[:config_file] it prints the lines of the file, as expected.
if options[:config_file]
File.new(options[:config_file], 'r').each { |params| puts params }
end
if options[:host_file]
File.new(options[:host_file], 'r').each { |host| puts host }
end
Looks like you are trying to use the OptionParser class in Ruby. Since it is not a part of the core library, make make sure to include the following at the top of your program:
require 'optparse'
Furthermore, make sure you are creating your options correctly:
options = {}
optparse = OptionParser.new do |opts|
options[:config_file] = nil
opts.on('-f', '--config-file', 'Enter the config file to open.') do
options[:dry_run] = true
end
end
optparse.parse!
When using a flag in command line, you are essentially setting a variable to true or false. For example, by default, -v (verbose) is set to false for most operations (like rm). Following the command and its optional flags are (sometimes required) command-line arguments, which is the filename in your case.
Calling your script should look similar to
$ ruby ./my_program.rb --config-file /path/to/some/file
^ ^ ^
program flag argument
As you saw, an optparse option must be a boolean. You only want to open a file if the flag is present (option is true). Just a slight change is needed for your program to run:
if options[:config_file]
File.new(ARGV[0], 'r').each { |params| puts params }
end
ARGV is an array of all command-line arguments to your script (following flags). If you only include one argument, you will want the first element, or index 0 (ARGV[0]). All arguments are separated by spaces. So if you implement the same technique for options[:host_file], you can use ARGV[1].

OptionParser to parse arguments form file instead of command line

I am using Ruby to execute a code that takes command line arguments.
now i trying to use the same program with differnt options so i am putting the options in a file and i want the program to read each line interpret the options and execute the program accordingly.
but i get this error. "C:/Ruby193/lib/ruby/1.9.1/optparse.rb:1348:in block in parse_in_order': undefined methodshift' for "--c execue --query unix --Servername abc123":String (NoMethodError)"
i understand that its reading the file and treating the line as string. but wondering if there is a way to overcome this shift error and treat the line as if it was entered in command prompt. or any better solution.
here is my code.
require 'optparse'
require 'micro-optparse'
# --command execue --query unix command --Servername abc123
f =File.open("list_of_commands.txt", "r")
f.each_line { |line|
line= line.chomp
#line = "--c execue --query unix --Servername abc123"
#line = eval("\"#{line}\"")
puts line
options = {}
OptionParser.new do |opts|
opts.on("-c", "--command result,execue,chart,scpfile", String, "Single command to execute ") do |c|
options[:comd] = c
end
opts.on("-q", "--query remote command, unix command", String, "performs the command on local or remote machine") do |q|
options[:query] = q
end
opts.on("-s", "--servername CHSXEDWDC002 ", String, "server name to execute the command") do |v|
options[:hname] = v
end
opts.on_tail('-h', '--help', 'Show this message') do
puts opts
exit
end
end.parse!(line)
p options
}
the contents of the file are below
--c execue --query unix --Servername abc123
i also tried to use micro-optparse but facing same error. any workaround ?
Update:
as per suggestion from "#mu is too short" i tried below options.
end.parse!("#{Shellwords.shellsplit(line)}") and/or
end.parse!(Shellwords.shellsplit(line)).
but none of them worked.
i also tried to split the line as array using "line = line.split("\t")" and then
end.parse!(line). out put as
--c execue
--query unix
--Servername abc123
but now i get error as block in : invalid option --c execute
Update:#2
looking at the error, the issue is with the wrong parameter(-c. but thanks to user "#mu is too short" for suggesting to use Array.
Update: 3
passing the array only worked for short form of the arguments such as -c but when long form was supplied it failed with invalid argument erorr.
i dont see much documentation on the optparse. i even tried micro-parse but it requres default valuves and its not an option for me :(
The parse! method wants an array as its argument, not a string. You'll probable want to use Shellwords.shellsplit rather than String#split (or similar hand-rolled method) to convert your line to an array just in case you have to deal with quoting and whatnot. Something like this:
OptionParser.new do |opts|
#...
end.parse!(Shellwords.shellsplit(line))
While you can put your command-line arguments into a file, flags and all, there are better ways to remember configuration settings.
Instead of storing the flags, use a YAML file. YAML is a great data format, that translates easily to Ruby hashes and objects. "Yaml Cookbook" is a very useful page for learning the ins and outs of the format with Ruby. There are YAML parsers for a myriad other languages, making it easy to share the settings, which can be useful as a system grows.
With a little creative code you can use your YAML as the base settings, and let your CLI flags override the stored settings.
If you're not familiar with YAML, it's easy to get a start on the file using something like:
require 'yaml'
data = {
'command' => %w[result execute chart scpfile],
'query' => ['remote command', 'unix command'],
'servername' => 'CHSXEDWHDC002',
}
puts data.to_yaml
Which outputs:
---
command:
- result
- execute
- chart
- scpfile
query:
- remote command
- unix command
servername: CHSXEDWHDC002
Redirect that output to a file ending in .yaml and you're on your way.
To read it back into a script use:
require 'yaml'
data = YAML.load_file('path/to/data.yaml')
A quick round-trip test shows:
require 'yaml'
data = {
'command' => %w[result execute chart scpfile],
'query' => ['remote command', 'unix command'],
'servername' => 'CHSXEDWHDC002',
}
YAML.load(data.to_yaml)
Which looks like:
{"command"=>["result", "execute", "chart", "scpfile"],
"query"=>["remote command", "unix command"],
"servername"=>"CHSXEDWHDC002"}
If you want to have defaults, stored in the YAML file, and override them with command-line flags, read the data from the file then use that resulting object as the base for OptionParse:
require 'optparse'
require 'yaml'
# Note, YAML can deal with symbols as keys, but other languages might not like them.
options = {
:comd => %w[result execute chart scpfile],
:query => ['remote command', 'unix command'],
:hname => 'CHSXEDWHDC002',
}
# we'll overwrite the options variable to pretend we loaded it from a file.
options = YAML.load(options.to_yaml)
OptionParser.new do |opts|
opts.on("-c", "--Command result,execue,chart,scpfile", String, "Single command to execute ") do |c|
options[:comd] = c
end
opts.on("-q", "--query remote command, unix command", String, "performs the command on local or remote machine") do |q|
options[:query] = q
end
opts.on("-s", "--Servername CHSXEDWHDC002 ", String, "server name to execute the command") do |v|
options[:hname] = v
end
opts.on_tail('-h', '--help', 'Show this message') do
puts opts
exit
end
end.parse!
That's not tested, but we do similar things at work all the time, so save it to a file and poke at it with a stick for a while, and see what you come up with.

Extracting filenames from command line arguments with Ruby

I'm trying to use optparse to parse command line arguments. I would like my program to accept arguments like that:
$ ./myscript.rb [options] filename
I can easily manage the [options] part:
require 'optparse'
options = { :verbose => false, :type => :html }
opts = OptionParser.new do |opts|
opts.on('-v', '--verbose') do
options[:verbose] = true
end
opts.on('-t', '--type', [:html, :css]) do |type|
options[:type] = type
end
end
opts.parse!(ARGV)
But how do I get the filename?
I could extract it manually from ARGV, but there has to be a better solution, just can't figure out how
The "parse" method returns the unprocessed ARGV. So in your example, it will return a one element array with the filename in it.
I can't just use ARGV.pop. For example
when the last argument is "css" it
could either be a file or belong to
--type switch.
But if your script requires the last argument to be a filename (which is what your usage output inquires) this case should never happen the script should exit with a non-zero and the user should get a usage report or error.
Now if you want to make a default filename or not require a filename as the last argument but leave it optional then you could just test to see if the last argument is a valid file. If so use it as expected otherwise continue without etc.
Hope this answer can still be useful.
Ruby has one built-in variable __FILE__ can do this type of work.
puts __FILE__
it will print out your file's name.
I don't think extracting it before sending it to OptionParser is bad, I think it makes sense. I probably say this because I have never used OptionParser before, but oh well.
require 'optparse'
file = ARGV.pop
opts = OptionParser.new do |opts|
# ...
end

Resources