8
\$\begingroup\$

I've been fiddling around with some programs, and since most of them are for fun, I haven't bothered with little things like making the output non-verbose until I, say, put it here, for review. I was idly wondering how I could make converting it easier, and naturally I thought of writing a program, because that's all I ever do. You could, of course, use a logging library and set the debug level, but that results in having lots of extra code everywhere that's never touched in production. With this, you can keep all that code, tag it, and when it's sent off to production

Here's how it works:

Read a line from standard input. If any state variables are nonzero, apply the action, decrement the variable, and move to the next line. If the line looks like a tag, set the appropriate state variable and go to the next line. If the line looks like it ends in a tag, apply the action from the tag to that line and go to the next. If it's still trying to figure out what to do, give up and output the line as-is.

debug_converter.rb

#USAGE: <input source> | ruby debug_converter.rb <comment symbol> > <output file>

# Hurr hurr I'm funny
#DEBUG[2] - comment
#print 'Enter the comment separator: '
#ARGV = gets.split

DEFAULT_ACTION = 'delete'

raise "Wrong number of args: #{ARGV.inspect}" if ARGV.length != 1

COMMENT_SYMBOL = ARGV[0]

deleting     = 0
commenting   = 0
uncommenting = 0
until (line = STDIN.gets).nil?
  line.chomp!
  if deleting != 0
    deleting -= 1
  elsif commenting != 0
    commenting -= 1
    puts line.gsub(/^(\s*)/, "\\1#{COMMENT_SYMBOL}")
  elsif uncommenting != 0
    uncommenting -= 1
    puts line.gsub(/^(\s*)#{COMMENT_SYMBOL}/, '\1')
  else
    #Helps to separate it conceptually from the bit that depends on the variables
    case line
      when /^[ \t]*#{COMMENT_SYMBOL}\s*DEBUG(?:\[(\d+)\])?\s*(?:-\s*([a-z]+))?/i
        line.strip!
        count, action = (Integer($1) rescue nil), $2 # For readability purposes.
        count  ||= 1
        action ||= DEFAULT_ACTION
        raise "Invalid count: #{count}" if count == 0
        case action
          when 'delete'    then deleting     = count.to_i
          when 'comment'   then commenting   = count.to_i
          when 'uncomment' then uncommenting = count.to_i
          else raise "'#{action}' is an invalid action!"
        end
      when /(.*)#{COMMENT_SYMBOL}\s*DEBUG(?:\s*-\s*([a-z]+))?\s*$/i
      line, action = $1, $2 # For readability
      action ||= DEFAULT_ACTION
      case action
        when 'delete'    then ; #Do nothing
        when 'comment'   then puts line.gsub(/^(\s*)/, "\\1#{COMMENT_SYMBOL}")
        when 'uncomment' then puts line.gsub(/^(\s*)#{COMMENT_SYMBOL}/, '\1')
        else raise "'#{action}' is an invalid action!"
      end
      else
        puts line
    end
  end
end

Now, say we call it like this:

test_file.txt | ruby debug_converter.rb # > test_file.prod.txt

If test_file.txt is this:

NORMAL TEXT:
normal text
    normal text with spaces
  normal text with tabs

VERBOSE FORMS:
#DEBUG[3] - comment
comment no indent
    comment with spaces
  comment with tab
#DEBUG[2] - delete
should be deleted
   should also be deleted
#DEBUG[2] - uncomment
#should be uncommented
    #should also be uncommented

IMPLICATIONS:
#DEBUG - comment
prevline implied 1 explicit comment
#DEBUG - delete
prevline implied 1 explicit delete
#DEBUG - uncomment
prevline implied 1 explicit uncomment
#DEBUG
prevline implied 1 implied delete

INLINES:
inline comment #DEBUG - comment
   inline comment with spaces #DEBUG - comment
inline delete #DEBUG - delete
inline implied delete #DEBUG
#inline uncomment #DEBUG - uncomment

then test_file.prod.txt will be this:

NORMAL TEXT:
normal text
    normal text with spaces
  normal text with tabs

VERBOSE FORMS:
#comment no indent
    #comment with spaces
  #comment with tab
should be uncommented
    should also be uncommented

IMPLICATIONS:
#prevline implied 1 explicit comment
prevline implied 1 explicit uncomment

INLINES:
#inline comment 
   #inline comment with spaces 
inline uncomment

Right now, I'm specifically looking for advice to do with:

  • Performance, with regards to both speed and memory usage. I'm fairly certain that this is about as good as it's gonna get (Ω(n) and Ω(1) respectively, for n=line count), but anything to make it faster is appreciated.
  • Usability. Note that this is meant to be used in, say, a Bash script, so it should be able to take input and push output from anywhere directly, without having to write to the filesystem and read it; that's why I'm taking from STDIN and giving to STDOUT instead of reading and writing to files supplied as arguments.
  • Extensibility. How can I make it easy to add new functions? Note that the one-line and multi-line functions, while they have the same name, act differently, so merging them isn't an option. I feel like I could turn deleting, commenting, and uncommenting into a Hash, but I can't figure out how it'd work, since each has to take different actions. Maybe I'm just tired.
  • Edge cases. I'm bad at figuring them out. Any help finding them all is appreciated.
\$\endgroup\$
3
  • \$\begingroup\$ Sorry for the wall of text Thank you for caring about posting a nice, readable question. I suggest that for each paragraph (newline separated piece of text) you stop and think. "If I only had 4-10 words how would I say that?" and add a small subtitle accordingly. After training it takes little time and makes your posts much more inviting to readers. \$\endgroup\$
    – Caridorc
    Commented Jun 24, 2015 at 20:55
  • 1
    \$\begingroup\$ @Caridorc Good idea! I'm currently working on another answer, but once I'm done with that, I'll do that. Thanks for the advice. \$\endgroup\$
    – anon
    Commented Jun 24, 2015 at 21:02
  • 1
    \$\begingroup\$ @Caridorc That any better? \$\endgroup\$
    – anon
    Commented Jun 24, 2015 at 21:48

1 Answer 1

2
\$\begingroup\$

Regarding extensibility/repetition:

  • Instead of having a seperate count for each action, use a single variable for counting and another variable for the current action. (This also eliminates any possibility of illegal state, i.e. multiple counters greater than zero)
  • You can use a hash to map action strings to lambdas that accept a line and output to STDOUT.
  • You can avoid repetition by noticing that a line with an inline action (e.g. foo #DEBUG - comment) is equivalent to an action line (#DEBUG - comment) followed by another line (foo). Alternatively, you can think of an action-only line as an empty line with an inline action.

When writing regexes, use \A and \z (start and end of string) instead of ^ and $ (start and end of line). It doesn't actually matter in your code, but it's generally needed for correct handling of multiline strings.

Suggested solution:

#USAGE: <input source> | ruby debug_converter.rb <comment symbol> > <output file>

raise "Wrong number of args: #{ARGV.inspect}" if ARGV.length != 1

DEFAULT_ACTION = 'delete'
COMMENT_SYMBOL = ARGV[0]

$actions = {
  'delete' => ->(line) { },
  'comment' => ->(line) { puts line.gsub(/^(\s*)/, "\\1#{COMMENT_SYMBOL}") },
  'uncomment' => ->(line) { puts line.gsub(/^(\s*)#{COMMENT_SYMBOL}/, '\1') }
}

$current_action = nil
$remaining_count = 0

def parse_action_command(str)
  str =~ /\A(?:\[(\d+)\])?\s*(?:-\s*([a-z]+))?\z/i
  count = Integer($1) rescue 1
  action = $2 || DEFAULT_ACTION
  raise "Invalid count: #{count}" if count < 0
  $current_action = action
  $remaining_count = count.to_i
end

def action_in_progress?
  $remaining_count > 0
end

def perform_current_action(line)
  action = $actions[$current_action]
  raise "Invalid action: #{$current_action}" unless action
  action.call(line)
  $remaining_count -= 1
end

LINE_WITH_ACTION_REGEX = /\A(.*)#{COMMENT_SYMBOL}\s*DEBUG((?:\[\d+\])?\s*(?:-\s*[a-z]+)?)\z/i
WHITESPACE_REGEX = /\A[ \t]*\z/

until (line = STDIN.gets).nil?
  line.chomp!
  if action_in_progress?
    perform_current_action(line)
  elsif line =~ LINE_WITH_ACTION_REGEX # split to code part and action part
    before_action, action = $1, $2
    parse_action_command(action)
    unless before_action =~ WHITESPACE_REGEX
      perform_current_action(before_action)
    end
  else
    puts line
  end
end
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.