#!/usr/bin/env ruby =begin = retab((-$Id: retab,v 1.5 2003/04/08 16:20:23 zunda Exp $-)) Converts tabs and spaces in the heads of lines of a souce file into tabs. This command is useful to * re-format indent with tabs and spaces mixed (made by e.g. vi with different tabstop and shiftwidth) which are odd looking on some of editors. You can do this by e.g. retab odd-looking-source.c In this case, the original file is renamed as odd-looking-source.c.retab and a re-formatted file is create as odd-looking-source.c. Original tabstops and shiftwidth maybe automatically detected by this command. Otherwise, they have to be specified with -t and -s options. and * eliminate tabs in ruby RD documents; auto-indent feature of vim sometimes puts tabs instead of spaces. This command usually fails to detect when indent of the other lines only consist of tabs (as they should do). In this case, you have to specify the tabstop like: retab -t 2 rd-with-tabs.rb == Usage retab [options] [filename(s)] retab -h Shows options. == Download The latest version of this program can be downaloaded from (()). == Copyright Copyright (C) 2003 zunda Permission is granted for use, copying, modification, distribution, and distribution of modified versions of this work under the terms of GPL version 2 or later. =end =begin ChangeLog * Tue Apr 8, 2003 zunda - better handling of backup files * Mon Apr 7, 2003 zunda - changed gsub! to gsub * Fri Apr 4, 2003 zunda - first release =end =begin == class Line A Line is a line of a source file. --- Line::new( line ) makes a new line from a String. --- Line::tabs --- Line::spcs retunrs number of tabs/spaces in the prefix. --- Line::retab( ts = 8, sw = 2 ) returns retab'd Line assuming that an original tab was seen as ((|ts|)) spaces, and the tabs in the retab'd Line will be seen as ((|sw|)) spaces. --- Line::expandtab( ts = 8 ) expands each tab in the prefix into ((|ts|)) spaces. --- Line::comment_begin? --- Line::comment_end? true if the line is beginning/ending of comments. Currently this only works for RD. --- Line::to_s returns the line as a String =end class Line def initialize( line, tabs = nil, spcs = nil ) @line = line @tabs = tabs @spcs = spcs end def tabs count_prefix unless @tabs @tabs end def spcs count_prefix unless @spcs @spcs end def retab( ts = 8, sw = 2 ) Line.new( @line.sub( /^[ \t]+/ ) { $&.gsub( /\t/, ' '*ts ).gsub( %r|#{' '*sw}|, "\t" ) } ) end def expandtab( ts = 8 ) Line.new( @line.sub( /^[ \t]+/ ) { $&.gsub( /\t/, ' '*ts ) } ) end def comment_begin? /^=begin/ =~ @line end def comment_end? /^=end/ =~ @line end def to_s @line end private def count_prefix @spcs = 0 @tabs = 0 @line.sub( /^[ \t]+/ ) do $&.each_byte do |c| case c.chr when ' ' @spcs += 1 when "\t" @tabs += 1 else break end end end end end =begin == Source Souce is a source file. A line of a Source is a Line. --- Source::new ( string ) makes a Source from the entire content of a source file. --- Source::ts_guess( ignore_comment = true, reject_portion = 0.3 ) --- Source::sw_guess( ignore_comment = true, reject_portion = 0.3 ) make a guess of tab stop (number of spaces shown for a tab) and shift width (number of characters shifted as an indent). If ((|ignore_comment|)), comment lines as defined by Line::comment_begin? and Line::comment_end? are not used for the guess. Also, ((|reject_portion|)) lines are ignored for the geuss. --- Line::retab( ts = 8, sw = 2, other_than_comment = true ) returns retab'd Line assuming that an original tab was seen as ((|ts|)) spaces, and the tabs in the retab'd Line will be seen as ((|sw|)) spaces. --- Line::expandtab( ts = 8, only_in_comment = true ) expands each tab in the prefix into ((|ts|)) spaces. --- Line::to_s returns the Source as a String. =end class Source def initialize( source ) @ts_guess = nil @sw_guess = nil @lines = Array.new if String == source.class then source.each do |line| @lines << Line.new( line ) end elsif Array == source.class then @lines = source.dup else raise TypeError, "Souce of a Source can not be a #{source.type}" end end def ts_guess( ignore_comment = true, reject_portion = 0.3 ) guess( true, reject_portion ) unless @ts_guess @ts_guess end def sw_guess( ignore_comment = true, reject_portion = 0.3 ) guess( true, reject_portion ) unless @sw_guess @sw_guess end def retab( ts = 8, sw = 2, other_than_comment = true ) r = Array.new each_with_comment_nest( other_than_comment ) do |line, nest| if nest < 1 then r << line.retab( ts, sw ) else r << line end end Source.new( r ) end def expandtab( ts = 8, only_in_comment = true ) r = Array.new each_with_comment_nest( only_in_comment ) do |line, nest| if nest < 1 then r << line else r << line.expandtab( ts ) end end Source.new( r ) end def to_s @lines.collect { |line| line.to_s }.join end private def each_with_comment_nest( see_comment = true ) comment_nest = 0 @lines.each do |line| if see_comment and line.comment_begin? then comment_nest += 1 end yield( line, comment_nest ) if see_comment and line.comment_end? then comment_nest -= 1 end end end def guess( ignore_comment = true, reject_portion = 0.3 ) # make a hash of histgram spchash = Hash.new comment = 0 each_with_comment_nest( ignore_comment ) do |line, nest| # count up if nest < 1 then unless spchash[line.spcs] then spchash[line.spcs] = [line.spcs, 1] else spchash[line.spcs][1] += 1 end end end # ignore appropriate lines and make an array of space counts spccount = Array.new ignored = 0 to_ignore = (@lines.size.to_f * reject_portion).to_i spchash.values.sort { |a, b| unless a[1] == b[1] then a[1] <=> b[1] else b[0] <=> a[0] end }.each do |spcs_and_count| ignored += spcs_and_count[1] next if ignored < to_ignore spccount << spcs_and_count[0] if spcs_and_count[0] > 0 end if spccount.size > 0 then @sw_guess = spccount.min @ts_guess = spccount.max + @sw_guess else @sw_guess = nil @ts_guess = nil end end end exit unless __FILE__ == $0 # Main routine require( 'getoptlong' ) def usage <<"USAGE" #{$0} [options] [filename(s)] - Converts tabs and spaces in the heads of lines of a souce file into tabs. options [defaults]: -t, --tabstop number of spaces shown for a tab in original file(s) [guess] -s, --shiftwidth number of spaces for an indent in original file(s) [guess] -b, --backup backup extension. The backup file will NOT be overwritten if it already exists. [.retab] -g, --only-guess only guesses tabstop and shiftwidth. -n, --no-rd force to ignore =begin and =end's [respects] -q, --quiet be quiet [a little bit verbose] USAGE end opt = GetoptLong.new( ['-t', '--tabstop', GetoptLong::REQUIRED_ARGUMENT], ['-s', '--shiftwidth', GetoptLong::REQUIRED_ARGUMENT], ['-n', '--no-rd', GetoptLong::NO_ARGUMENT], ['-b', '--backup', GetoptLong::OPTIONAL_ARGUMENT], ['-g', '--only-guess', GetoptLong::NO_ARGUMENT], ['-q', '--quiet', GetoptLong::NO_ARGUMENT], ['-h', '--help', GetoptLong::NO_ARGUMENT] ) opt_ts = nil opt_sw = nil opt_with_rd = true opt_back = '.retab' opt_edit = true opt_verbose = true begin opt.each do |on, ov| case on when '-t' opt_ts = ov.to_i when '-s' opt_sw = ov.to_i when '-n' opt_with_rd = false when '-b' opt_back = ov if ov when '-g' opt_edit = false when '-q' opt_verbose = false when '-h' puts usage exit end end rescue exit 1 end ARGV << '-' if ARGV.empty? ARGV.each do |filename| # check the backup backfilename = nil if opt_edit and ('-' != filename) then unless opt_back.empty? then backfilename = filename + opt_back if FileTest.exist?( backfilename ) then $stderr.puts( "Backup file #{backfilename} already exists. Remove it first.") next end end orig_mode = File.stat( filename ).mode orig_mtime = File.stat( filename ).mtime end # read the file unless filename == '-' then f = File.open( filename ) else f = $stdin end s = Source.new( f.read || '' ) f.close # guess ts = opt_ts || s.ts_guess( opt_with_rd ) || 0 sw = opt_sw || s.sw_guess( opt_with_rd ) || 0 if opt_verbose then $stderr.puts "#{filename}: ts:#{ts} sw:#{sw}" end next unless opt_edit next if ts == 0 and sw == 0 # edit if ts > 0 and sw > 0 then s = s.retab( ts, sw, opt_with_rd ) end if ts > 0 then s = s.expandtab( ts, opt_with_rd ) end # output unless '-' == filename then File.rename( filename, backfilename ) f = File.open( filename, 'w' ) else f = $stdout end f.print s.to_s unless '-' == filename then f.close File.chmod( orig_mode, filename ) end end