#!/usr/bin/env ruby # nopaste # # Version 2.0 # # Paste files directly to the rafb.net nopaste service from the command line. # Demonstrates the use of OptionParser and Net::HTTP for POST requests. # Verbose comments for the benefit of Ruby learners. # # This code may be used and modified freely, providing that credit is given. # # Paul Battley 2004-08-16 * http://po-ru.com/ require 'optparse' require 'net/http' require 'cgi' require 'uri' class NoPaste # Custom error class for when we find a dodgy option class OptionError < RuntimeError end # All the highlighters supported by rafb.net. # Name => [extensions, shebang matching expression, magic expression] LANGUAGES = { 'Plain Text' => [%w[txt]], 'C89' => [%w[]], 'C' => [%w[c h m]], 'C++' => [%w[cpp c++ hpp]], 'C#' => [%w[cs]], 'Java' => [%w[java]], 'Pascal' => [%w[pas]], 'Perl' => [%w[pl pm], /^#!.*perl/], 'PHP' => [%w[php]], 'PL/I' => [%w[]], 'Python' => [%w[py], /^#!.*python/], 'Ruby' => [%w[rb rbw], /^#!.*ruby/], 'SQL' => [%w[sql]], 'VB' => [%w[bas]], 'XML' => [%w[xml xsl], nil, / nil, "cvt_tabs" => "No", "nick" => "", "desc" => "" } begin File.open(File.join(ENV['HOME'], '.nopasterc')).each_line do |ln| ln.strip! if (ln =~ /[^#].*=.+/) k, v = ln.split(/\s+=\s+/, 2) # Check that it's a known option if (options.keys.include?(k)) options[k] = v else STDERR.puts("Invalid option in rc file: #{ln}") end end end rescue # Don't care if we couldn't open the rc file end # Create a hash that maps lowercase names to their official codes so # that the interface is case-insensitive. hlmap = Hash.new LANGUAGES.keys.each do |h| hlmap[h.downcase] = h end # Set up the option parser and the messages for each option. Raise an # OptionError if anything is amiss. opts = OptionParser.new do |opt| opt.banner = "nopaste - paste one or more files to the nopaste pages at http://rafb.net/paste/" opt.separator(" ") opt.separator("Usage: nopaste [options] filename(s)") opt.separator(" ") opt.separator("Default options may be specified, one per line (format is 'field = value')") opt.separator("in a .nopasterc file in the users home directory.") opt.separator(" ") opt.separator("Use - instead of filename to read from standard input.") opt.separator(" ") opt.separator("Options:") # argument LANGUAGE is mandatory opt.on( "-l", "--language LANGUAGE", "Use syntax highlighter for LANGUAGE.", "Default is to auto-detect (falls back to \"#{DEFAULT_LANG}\")." ) do |highlighter| if (hlmap[highlighter.downcase]) options["lang"] = hlmap[highlighter.downcase] else raise OptionError, "Unknown language \"#{highlighter}\"." end end opt.on( "-n", "--nick NICK", "User nickname (max. 9 characters)." ) do |nick| if (nick.length <= 9) options["nick"] = nick else raise OptionError, "Nick too long." end end opt.on( "-d", "--description DESCRIPTION", "Description of paste (max. 50 characters)." ) do |desc| if (desc.length <= 50) options["desc"] = desc else raise OptionError, "Description too long." end end opt.on( "-t", "--convert-tabs N", "Convert tabs to N spaces.", "Default is to leave tabs as-is." ) do |tabs| if (tabs.to_i >= 2 and tabs.to_i <= 8) options["cvt_tabs"] = tabs.to_i else raise OptionError, "Number of spaces for tab conversion must be between 2 and 8." end end opt.on( "-i", "--highlighters", "List the available syntax highlighters." ) do HighlighterCodes.each do |h| puts(h) end end opt.on_tail("-h", "--help", "Show this message") do puts opt exit end end # Actual parsing starts here; even though it calls the code segments # written above, error trapping must be performed below. Note that we # trap OptionParser::MissingArgument as well as our own OptionError; # this is raised by OptionParser when a mandatory argument is missing. begin opts.parse!(args) raise OptionError, "No source file(s) specified" if (args.empty?) rescue OptionError, OptionParser::MissingArgument => err puts "Error: " + err.to_s + "\n" puts opts exit end @options = options end # Cycle through each item in files, read it from disk, and pass the # contents to post def upload(files) files.each do |file| begin if (file == '-') $stderr.puts("Type Control-D to end.") text = STDIN.read uri = post('', text) puts("Standard input => #{uri}") else text = File.read(file) uri = post(file, text) puts("#{file} => #{uri}") end rescue # If something went wrong posting the data, we will report # this, and carry on to the next file (if there are any more) puts("#{file} => Error: #{$!.to_s}") end end end # Post the contents of text to the nopaste service with the options we have # specified. def post(filename, text) # We use .dup to get a copy of @options. Without .dup, modifications # to data would modify the @options hash as well. data = @options.dup data["lang"] = guess_language(filename, text) data["text"] = text data["desc"] = filename if data["desc"].empty? # Join everything in data into the POST data format: k1=v1&k2=v2...kN=vN # escaping it appropriately post_string = data.keys.map{ |k| "#{k}=#{CGI.escape(data[k].to_s)}" }.join("&") uri = URI.parse(SUBMIT_URI) h = Net::HTTP.new(uri.host, uri.port) response, body = h.post(uri.path, post_string) return URI.join(SUBMIT_URI, response["location"]).to_s end def initialize(args) index_languages parse(args) # Note that args will now contain only those items that were not parsed # as options. upload(args) end end # Pass the command-line arguments to NoPaste NoPaste.new(ARGV)