I have a ruby cli parsing script and it seems that there is some sort of regex behavior for parsing options:
op = OptionParser.new do |x|
x.on("--output-config PATH", "The filesystem location for the output config file") do |output_config|
options[:output_config] = output_config
end
x.on("-j", "--json", "If this is set, then json is output instead of tabular form") do
options[:disp_json] = true
end
x.on("-h", "--help", "Show this message") do
puts op
exit 0
end
x.on("-v", "--version", "Show version") do
puts "version #{VERSION_NUMBER}"
exit 0
end
end
# do input validation and check leftovers for proper commands
begin
# parse options, parse! removes elements from ARGV so leftovers are positional arg(s)
op.parse!(ARGV)
options[:config_file] = ARGV[0] if ARGV[0]
rescue OptionParser::InvalidOption, OptionParser::MissingArgument
puts "############### #{$!.to_s} ###############"
puts ""
puts op
exit 1
end
Then if I call it as such:
script -a
It outputs the following (expected behavior)
############### invalid option: -a ###############
Or
script --output-config
It outputs the following (expected behavior)
############### missing argument: --output-config ###############
So this is where it gets odd:
script --output
It outputs the following (not expected behavior)
############### missing argument: --output ###############
Or
script --ou
It outputs the following (not expected behavior)
############### missing argument: --ou ###############
Basically anything you pass that regex matches "output-config" is passed to the block for
x.on("--output-config PATH"....
Which is the cause of the MissingArgument vs InvalidOption behavior I'm seeing.
Am I using optparse wrong or is this a bug in the library?
####### EDIT
If I add another x.on:
x.on("--out PATH", "The filesystem location for the output config file") do |output_config|
options[:output_config2] = output_config
end
And pass either -o (one dash, ie a short form) or --o (two dashes), it does not throw an exception (I specifically am rescuing OptionParser::AmbiguousOption at the handling is not executed). Instead is executes the shortest match, which is --out. If I pass --outp, then the longer one is executed. This seems flaky to me.
####### EDIT 2
> ./my_app --output-c
############### missing argument: --output-c
The MissingArgument exception only displays the flag as passed, not as 'intended'. It clearly knows that it is matching against '--output-config' so I'd like to be able to know that so that my error message to the user is clear and explicit. Is there a way I can determine what optparser was matching against at the time that the MissingArgument exception was raised?
This is standard behavior with long options. I'm having trouble finding any supporting documentation but I've been using this feature of long options as long as I've been using long options (i.e. since a long long time ago).
You can see it easily with the GNU version of ls(1):
$ ls --h
ls: option '--h' is ambiguous
Try `ls --help' for more information.
$ ls --he
Usage: ls [OPTION]... [FILE]...
List information about the FILEs (the current directory by default).
Sort entries alphabetically if none of -cftuvSUX nor --sort.
...
$ ls --help
Usage: ls [OPTION]... [FILE]...
List information about the FILEs (the current directory by default).
Sort entries alphabetically if none of -cftuvSUX nor --sort.
...
There are multiple options that begin with --h so --h is an options error, there is only one option that starts with --he so --he is the same as --help.
If you add an --output-pancakes option, then you should get a complaint about ambiguity if you say --output but --output-c and --output-p will work.
You're not using the library incorrectly. Nor is this a bug. This is a feature.
Related
I have installed the Ruby environment manager rbenv, Ruby, RubyGems, and PNGlitch (on macOS). Now how do I use PNGlitch?
The best documentation I have been able to find is on this page, and here's an example of the syntax given:
How to use this library: The Simple Way
png = PNGlitch.open '/path/to/your/image.png'
png.glitch do |data|
data.gsub /\d/, 'x'
end
png.save '/path/to/broken/image.png'
png.close
Okay, great. When I insert my file paths, save that code as an .rb file, and open it, I just get:
test.rb: command not found
If I paste it directly into Terminal I get:
png = PNGlitch.open '/Users/username/Documents/testimage.png'
-bash: png: command not found
ComputerName:~ username$ png.glitch do |data|
> data.gsub /\d/, 'x'
-bash: png.glitch: command not found
-bash: data: command not found
-bash: data.gsub: command not found
ComputerName:~ username$ end
-bash: end: command not found
ComputerName:~ username$ png.save '/Users/username/Documents/testimage_glitched.png'
-bash: png.save: command not found
ComputerName:~ username$ png.close
I also tried the syntax given on this page and entered:
pnglitch /Users/username/Documents/testimage.png –filter=Sub /Users/username/Documents/testimage_glitched.png
...this resulted in getting the following message:
tried to create Proc object without a block
Usage:
pnglitch <infile> [--filter=<n>] <outfile>
Options:
-f, --filter=<n> Fix all filter types as passed value before glitching.
A number (0..4) or a type name (none|sub|up|average|paeth).
--version Show version.
-h, --help Show this screen.
↑ I guess this is the developer's idea of documentation. 🤣
Well, trying to follow that example I also did this:
pnglitch </Users/username/Documents/testimage.png> [--filter=<2>] </Users/username/Documents/testimage_glitched.png>
...but that only resulted in:
-bash: syntax error near unexpected token 2
(I chose 2 because apparently that corresponds to the "Sub" filter.)
I tried variants of this syntax as well, including omitting characters <> and [].
There must be some assumed knowledge here that I don't have. So what I would like to know is:
How can I actually use PNGlitch to glitch a PNG image?
How can I use PNGlitch to glitch all the PNG images in a folder?
Any additional advice on using different filters would also be appreciated.
Thank you.
There's a lot going on here that needs to be cleared up.
Ruby scripts need Ruby to run
You can't just paste these into bash and expect anything useful to happen.
The usual procedure is one of two variants. Either:
Create a .rb script, like example.rb
Run ruby example.rb where that's your script name at the end.
Or use the "hash-bang" method:
Create a script with #!/usr/bin/env ruby as the very first line.
Make this script executable with chmod +x example.rb
Run this script directly, ./example.rb or whatever path it has.
Note that example.rb by itself will not work unless it is in your path, hence the ./ is necessary.
Command line example syntax
Here <name> has special meaning, where it's just a way of saying name as if it had italics or special formatting. On a text-mode terminal it's not practical to add syntax like that, it's limited to ASCII in most cases, so this tradition evolved.
Within the POSIX shell > and < have special meaning, they're used to, respectively, redirect input to or from a file. For example, ls > ls.txt dumps the output of ls into a file called ls.txt, while cat < ls.txt reads in the contents of ls.txt and displays it.
Things like [name] mean optional arguments, like [--help] means the --help argument is optional.
Within the POSIX shell [ and ] have special meaning. They can be used in an if construct, but more commonly in file wildcards, like l[abc].txt means any of la.txt, lb.txt or lc.txt.
Putting this together it's possible to understand the notation used here:
pnglitch <infile> [--filter=<n>] <outfile>
Where that means infile is your "input file" argument, and outfile is your "output file" argument, and --filter is an optional argument taking n as an input.
So you call it like this:
pnglitch input.png output.png
Or with an option, like you did:
pnglitch testimage.png --filter=sub testimage_glitched.png
Though note I've used lower-case sub as that's precisely what's in the help output and following casing conventions usually matters.
I have to pass a command with its arguments in a scheduled task, while separating the arguments from the command. I used:
split(/(?=\s-)/)
to do this, but it won't work when the argument is not passed as -arg format.
Example of commands can be passed in format:
"ping http://www.google.com" here url is argument
"abc-abc -V"
"abc-abc -L c:\\folder name\\test.log"
'"C:\\Program Files\\example\\program.exe" -arg1 -arg2'
"C:\\Program Files\\example\\program.exe"
To make this more clear these commands are not passed as command line argument which can get in ARGV
The command gets set in command property which accepts input in string format
command '"C:\\Program Files\\example\\program.exe" -arg1 -arg2'
Use Shellwords.split, from the standard library:
Shellwords.split("ping http:\\www.google.com here url is argument")
#=> ["ping", "http:www.google.com", "here", "url", "is", "argument"]
Shellwords.split("abc-abc -V")
#=> ["abc-abc", "-V"]
Shellwords.split("abc-abc -L c:\\folder name\\test.log")
#=> ["abc-abc", "-L", "c:folder", "nametest.log"]
Shellwords.split('"C:\\Program Files\\example\\program.exe" -arg1 -arg2')
#=> ["C:\\Program Files\\example\\program.exe", "-arg1", "-arg2"]
Shellwords.split('"C:\\Program Files\\example\\program.exe"')
#=> ["C:\\Program Files\\example\\program.exe"]
No need to reinvent the wheel with a custom regex/splitter, or an external system call.
It seems to me that if there's no consistent pattern to your command syntax, then any regex based approach will inevitably fail. It seems better instead to solve this problem the way a human would, i.e. with some knowledge of context.
In a *nix terminal, you can use the compgen command to list available commands. This Ruby script invokes that command to print the first 5 options from that list:
list = `cd ~ && compgen -c`
list_arr = list.split("\n")
list_arr[0,6].each{|x| puts x }
(The cd in the first line seems to be needed because of the context in which my Ruby is running with rvm.) For Windows, you may find this thread a useful starting point.
I'd match against the elements of this list to identify my commands, and take it from there.
Tom Lord's answer is far better than this one.
You probably want to look at OptionParser or GetOptLong if you need parsing of command line arguments provided to a ruby program.
If you are interested in parsing some strings that may or may not be commands with arguments, here's a quick-and-dirty:
I'd use scan instead of split with the following regex: /(".*"|[\w\:\:\.\-\\]+)/.
Best results come from: 'some string'.scan(/(".*"|[\w\:\:\.\-\\]+)/).flatten:
["ping", "http:\\www.google.com"]
["abc-abc", "-V"]
["abc-abc", "-L", "c:\\folder\\", "name\\test.log"]
# Technically, this is wrong, but so is the non-escaped whitespace.
["\"C:\\Program Files\\example\\program.exe\"", "-arg1", "-arg2"]
["\"C:\\Program Files\\example\\program.exe\""]
When you type something, you often use bash autocompletion: you start writing a command, for example, and you type TAB to get the rest of the word.
As you have probably noticed, when multiple choices match your command, bash displays them like this :
foobar#myserv:~$ admin-
admin-addrsync admin-adduser admin-delrsync admin-deluser admin-listsvn
admin-addsvn admin-chmod admin-delsvn admin-listrsync
I'm looking for a solution to display each possible solution on a new line, similar to the last column on a ls -l. Ever better, it would be perfect if I could apply a rule like this: "if you find less than 10 suggestions, display them one by line, if more => actual display".
bash prior to version 4.2 doesn't allow any control over the output format of completions, unfortunately.
Bash 4.2+ allows switching to 1-suggestion-per-line output globally, as explained in Grisha Levit's helpful answer, which also links to a clever workaround to achieve a per-completion-function solution.
The following is a tricky workaround for a custom completion.
Solving this problem generically, for all defined completions, would be much harder (if there were a way to invoke readline functions directly, it might be easier, but I haven't found a way to do that).
To test the proof of concept below:
Save to a file and source it (. file) in your interactive shell - this will:
define a command named foo (a shell function)
whose arguments complete based on matching filenames in the current directory.
(When foo is actually invoked, it simply prints its argument in diagnostic form.)
Invoke as:
foo [fileNamePrefix], then press tab:
If between 2 and 9 files in the current directory match, you'll see the desired line-by-line display.
Otherwise (1 match or 10 or more matches), normal completion will occur.
Limitations:
Completion only works properly when applied to the LAST argument on the command line being edited.
When a completion is actually inserted in the command line (once the match is unambiguous), NO space is appended to it (this behavior is required for the workaround).
Redrawing the prompt the first time after printing custom-formatted output may not work properly: Redrawing the command line including the prompt must be simulated and since there is no direct way to obtain an expanded version of the prompt-definition string stored in $PS1, a workaround (inspired by https://stackoverflow.com/a/24006864/45375) is used, which should work in typical cases, but is not foolproof.
Approach:
Defines and assigns a custom completion shell function to the command of interest.
The custom function determines the matches and, if their count is in the desired range, bypasses the normal completion mechanism and creates custom-formatted output.
The custom-formatted output (each match on its own line) is sent directly to the terminal >/dev/tty, and then the prompt and command line are manually "redrawn" to mimic standard completion behavior.
See the comments in the source code for implementation details.
# Define the command (function) for which to establish custom command completion.
# The command simply prints out all its arguments in diagnostic form.
foo() { local a i=0; for a; do echo "\$$((i+=1))=[$a]"; done; }
# Define the completion function that will generate the set of completions
# when <tab> is pressed.
# CAVEAT:
# Only works properly if <tab> is pressed at the END of the command line,
# i.e., if completion is applied to the LAST argument.
_complete_foo() {
local currToken="${COMP_WORDS[COMP_CWORD]}" matches matchCount
# Collect matches, providing the current command-line token as input.
IFS=$'\n' read -d '' -ra matches <<<"$(compgen -A file "$currToken")"
# Count matches.
matchCount=${#matches[#]}
# Output in custom format, depending on the number of matches.
if (( matchCount > 1 && matchCount < 10 )); then
# Output matches in CUSTOM format:
# print the matches line by line, directly to the terminal.
printf '\n%s' "${matches[#]}" >/dev/tty
# !! We actually *must* pass out the current token as the result,
# !! as it will otherwise be *removed* from the redrawn line,
# !! even though $COMP_LINE *includes* that token.
# !! Also, by passing out a nonempty result, we avoid the bell
# !! signal that normally indicates a failed completion.
# !! However, by passing out a single result, a *space* will
# !! be appended to the last token - unless the compspec
# !! (mapping established via `complete`) was defined with
# !! `-o nospace`.
COMPREPLY=( "$currToken" )
# Finally, simulate redrawing the command line.
# Obtain an *expanded version* of `$PS1` using a trick
# inspired by https://stackoverflow.com/a/24006864/45375.
# !! This is NOT foolproof, but hopefully works in most cases.
expandedPrompt=$(PS1="$PS1" debian_chroot="$debian_chroot" "$BASH" --norc -i </dev/null 2>&1 | sed -n '${s/^\(.*\)exit$/\1/p;}')
printf '\n%s%s' "$expandedPrompt" "$COMP_LINE" >/dev/tty
else # Just 1 match or 10 or more matches?
# Perform NORMAL completion: let bash handle it by
# reporting matches via array variable `$COMPREPLY`.
COMPREPLY=( "${matches[#]}" )
fi
}
# Map the completion function (`_complete_foo`) to the command (`foo`).
# `-o nospace` ensures that no space is appended after a completion,
# which is needed for our workaround.
complete -o nospace -F _complete_foo -- foo
bash 4.2+ (and, more generally, applications using readline 6.2+) support this with the use of the completion-display-width variable.
The number of screen columns used to display possible matches when performing completion. The value is ignored if it is less than 0 or greater than the terminal screen width. A value of 0 will cause matches to be displayed one per line. The default value is -1.
Run the following to set the behavior for all completions1 for your current session:
bind 'set completion-display-width 0'
Or modify your ~/.inputrc2 file to have:
set completion-display-width 0
to change the behavior for all new shells.
1 See here for a method for controlling this behavior for individual custom completion functions.
2 The search path for the readline init file is $INPUTRC, ~/.inputrc, /etc/inputrc so modify the file appropriate for you.
I'm working on implementing Project Euler solutions as semantic Ruby one-liners. It would be extremely useful if I could coerce Ruby to automatically puts the value of the last expression. Is there a way to do this? For example:
#!/usr/bin/env ruby -Ilib -rrubygems -reuler
1.upto(100).into {|n| (n.sum.squared - n.map(&:squared).sum)
I realize I can simply puts the line, but for other reasons (I plan to eval the file in tests, to compare against the expected output) I would like to avoid an explicit puts. Also, it allots me an extra four characters for the solution. :)
Is there anything I can do?
You might try running it under irb instead of directly under a Ruby interpreter.
It seems like the options -f --noprompt --noverbose might be suitable (.
#!/usr/bin/env irb -f --noprompt --noverbose -Ilib -rrubygems -reuler
'put your one-liner here'
The options have these meanings:
-f: do not use .irbrc (or IRBRC)
--noverbose: do not display the source lines
--noprompt: do not prefix the output (e.g. with =>)
result = calculate_result
puts result if File.exist?(__FILE__)
result of eval is last executed operation just like any other code block in ruby
is doing
puts eval(file_contents)
an option for you?
EDIT
you can make use of eval's second parameter which is variables binding
try the following:
do_not_puts = true
eval(file_contents, binding)
and in the file:
....
result = final_result
if defined?(do_not_puts)
result
else
puts(result)
end
Is it an option to change the way you run scripts?
script.rb:
$_= 1.upto(100).into {|n| (n.sum.squared - n.map(&:squared).sum)
invoke with
echo nil.txt | /usr/bin/env/ruby -Ilib -rrubygems -reuler -p script.rb, where nil.txt is a file with a single newline.
I wrote a simple script that writes all given arguments to a single text file, separated by newline. I'd like to pass a list of files to it using OptionParser. I would like to add a couple of files using wildcards like /dir/*.
I tried this:
opts = OptionParser.new
opts.on('-a', '--add FILE') do |s|
puts "DEBUG: before #{s}"
#options.add = s
puts "DEBUG: after #{#options.add}"
end
...
def process_arguments
#lines_to_add = Dir.glob #options.add
end
Put when I add files like this:
./script.rb -a /path/*
I always get only the first file in the directory. All the debug outputs show only the first file of directory, and it seems as if OptionParser does some magic interpretations
Does anyone know how to handle this?
You didn't mention which operating system you are using (it matters).
On Windows, whatever you type on the command line gets passed to the program without modification. So if you type
./script.rb -a /path/*
then the arguments to the program contain "-a" and "/path/*".
On Unix and other systems with similar shells, the shell does argument expansion that automatically expands wildcards in the command line. So when you type the same command above, the shell looks to find the files in the /path/* directory and expands the command line arguments before your program runs. So the arguments to your program might be "-a", "/path/file1", and "/path/file2".
An important point is that the script cannot find out whether argument expansion happened, or whether the user actually typed all those filenames out on the command line.
As mentioned above, the command-line is being parsed before the OS hands off the command to Ruby. The wildcard is being expanded into a list of space-delimited filenames.
You can see what will happen if you type something like echo * at the command-line, then, instead of hitting Return, instead hit Esc then *. You should see the * expanded into the list of matching files.
After hitting Return those names will be added to the ARGV array. OptionParser will walk through ARGV and find the flags you defined, grab the following elements if necessary, then remove them from ARGV. When OptionParser is finished any ARGV elements that didn't fit into the options will remain in the ARGV array where you can get at them.
In your code, you are looking for a single parameter for the '-a' or '--add FILE' option. OptionParser has an Array option which will grab comma-separated elements from the command line but will subsequent space-delimited ones.
require 'optparse'
options = []
opts = OptionParser.new
opts.on('-a', '--add FILE', Array) do |s|
options << s
end.parse!
print "options => ", options.join(', '), "\n"
print "ARGV => ", ARGV.join(', '), "\n"
Save that to a file and try your command line with -a one two three, then with -a one,two,three. You'll see how the Array option grabs the elements differently depending on whether there are commas or spaces between the parameters.
Because the * wildcard gets replaced with space delimited filenames you'll have to post-process ARGV after OptionParser has run against it, or programmatically glob the directory and build the list that way. ARGV has all the files except the one picked up in the -a option so, personally, I'd drop the -a option and let ARGV contain all the files.
You will have to glob the directory if * has to too many files and exceeds the buffer size. You'll know if that happens because the OS will complain.
The shell is expanding the argument before it gets passed to your program. Either keep consuming filenames until you reach another option, or have the user escape the wildcards (e.g. ./script.rb -a '/path/*') and glob them yourself.
What's happening is the shell is expanding the wildcard before Ruby gets to it. So really you are processing:
./script.rb -a /path/file1 /path/file2 ......
Put quotes around /path/* to avoid the shell expansion and pass the wildcard to Ruby:
./script.rb -a '/path/*'