Finally found solution in "Using ruby's OptionParser to parse sub-commands".
I need to use the poorly documented order method which will parse options only up to first positional argument. parse will parse the whole ARGV:
require 'optparse'
options = {}
filenames = []
file_options = Hash.new do |hash, key| hash[key] = {} end
filename = nil
file_parser = OptionParser.new do |o|
  o.banner = 'Options for every file (goes after every filename):'
  o.on '-o', '--outfile=FILE', 'A file to write result to' do |arg|
    file_options[filename][:outfile] = arg
  end
end
global_parser = OptionParser.new do |o|
  o.banner = [
    "Super duper file processor.\n",
    "Usage: #{$PROGRAM_NAME} [options] file1 [file1-options] [file2 [file2-options] …]",
  ].join("\n")
  o.separator ''
  o.separator 'Common options:'
  o.on '-v', '--verbose', 'Display result filenames' do |arg|
    options[:verbose] = arg
  end
  o.on '-h', '--help', 'Print this help and exit' do
    $stderr.puts opts
    exit
  end
  o.separator ''
  o.separator file_parser.help
end
begin
  argv = global_parser.order(ARGV)
  while (filename = argv.shift)
    filenames.push(filename)
    file_parser.order!(argv)
  end
rescue OptionParser::MissingArgument => e
  $stderr.puts e.message
  $stderr.puts global_parser
  exit 1
end
if filenames.empty?
  $stderr.puts global_parser
  exit 1
end
If I execute my script like this:
script.rb -v a -o a.out b c d -o d.out
I will get:
options      = {:verbose=>true}
file_options = {"a"=>{:outfile=>"a.out"}, "d"=>{:outfile=>"d.out"}}
And this help text will be generated:
Super duper file processor.
Usage: script.rb [options] file1 [file1-options] [file [file2-options]…]
Common options:
    -v, --verbose                    Display converted filenames
    -h, --help                       Print this help and exit
Options for every file (goes after every filename):
    -o, --outfile=FILE               A file to write result to