summaryrefslogblamecommitdiff
path: root/pds
blob: ff6d725ece3a9040c8048e28552b5846e49f974f (plain) (tree)
1
2
3
4
5
6
7
8
9
10
11



                                                                  
                                                                      





                                                                                

                                                                               

















                                                                               
             


















                  

                





























                                                     






                         





                                                  

                                                 












                                                 







                                

                           
                                                 





















































                                                                                

                                            



                                                            
                                        

















































































                                                                              
#!/usr/bin/ruby
# pds - parallel data substitution (doing 80% of sed 120% as well)

USAGE = <<'EOF'
pds [-i[BAK]] [-0] [-A] [-g] [-r] [-p] [-t] [-w] [-c[CHECK]] [-n[NTH]]
    PATTERN REPLACEMENT [PATTERN REPLACEMENT]... -- FILES...
             replaces each PATTERN with REPLACEMENT in the input, simultaneously

  -i[BAK]    edit files in place (backup with suffix BAK if supplied)
  -0         work on NUL terminated lines
  -A         work on whole file at once
  -g         guarded replacement: uses additional PATTERN for each REPLACEMENT,
             the first needs to match as regexp anywhere on the line
  -r         enable regex for PATTERN and backreferences for REPLACEMENT
             \& for the match, \1..\9 for n-th backreference, \c to clear line
  -p         only print lines with replacements
  -t         toggle; also replace each REPLACEMENT with PATTERN
  -w         PATTERN shall only match whole words
  -c[CHECK]  ensure there has been at least one (or CHECK) replacement per file
  -n[NTH]    only replace NTH match (comma-separated list of numbers) per line

EOF

require 'fcntl'

def fatal(msg)
  STDERR.puts "pds: #{msg}"
  exit -1
end

iflag = nil
gflag = false
rflag = false
nullflag = true
nflag = []
cflag = nil
allflag = false
pflag = false
tflag = false
wflag = false

replacements = []
nl = "\n"

done = false
until done
  arg = ARGV.shift

  case arg
  when /\A-i/
    iflag = $'
  when "-g"
    gflag = true
  when "-r"
    rflag = true
  when "-p"
    pflag = true
  when "-t"
    tflag = true
  when "-w"
    wflag = true
  when "-0"
    nl = "\0"
    nullflag = true
  when /\A-c/
    cflag = $'
  when "-A"
    allflag = true
  when /\A-n/
    nflag.concat $'.split(',').map { |x| Integer(x) }
  when /\A-/
    fatal "invalid argument '#{arg}'\n#{USAGE}" 
  else
    if rflag && tflag
      fatal "cannot use -r and -t together"
    end

    loop {
      if !arg || arg == "--"
        done = true
        break
      end

      if gflag
        guard = arg
        from = ARGV.shift
      else
        guard = nil
        from = arg
      end
      to = ARGV.shift
      if !to
        STDERR.puts "no replacement for '#{from}'"
        exit 1
      end

      replacements << [guard, from, to]
      replacements << [guard, to, from]  if tflag

      arg = ARGV.shift
    }
  end
end
if cflag
  cflag = cflag.split(',').map { |x| Integer(x) }
end

if replacements.empty?
  fatal "no pattern given\n#{USAGE}"
end

gx = replacements.map { |x|
  begin
    x[0] && Regexp.new(x[0])
  rescue RegexpError
    fatal "invalid regex: #{$!}"
  end
}

rx = replacements.map { |x|
  begin
    rflag ? Regexp.new(x[1]) : Regexp.quote(x[1])
  rescue RegexpError
    fatal "invalid regex: #{$!}"
  end
}
if wflag
  rx.map! { |x| /\b#{x}\b/ }
end
union = Regexp.union(rx)

retval = 0

ARGV << "-"  if ARGV.empty?
ARGV.each { |file|
  input =
    if file == "-"
      STDIN
    else
      begin
        File.open(file)
      rescue SystemCallError => e
        fatal "can't read #{file}: #{e.to_s.sub(/ @ .*/, '')}"
      end
    end
  output =
    if iflag
      begin
        tmpname = "#{file}.pds.#{rand(2**32).to_s(36)}~"
        File.open(tmpname, Fcntl::O_WRONLY | Fcntl::O_EXCL | Fcntl::O_CREAT)
      rescue SystemCallError => e
        fatal "couldn't open temporary file #{file}: #{e.to_s.sub(/ @ .*/, '')}"
      end
    else
      STDOUT
    end

  reps = 0

  while line = (allflag ? input.read : input.gets(nl))
    newline = ""
    lastpos = 0
    offset = 0
    nth = 0

    if nullflag
      # fix $ and \z behavior
      line.chomp!(nl)
    end

    loop {
      leftmost = nil
      matched = nil
      replace = nil

      rx.each_with_index { |r, i|
        next  if gx[i] && !line.match(gx[i])

        if m = line.match(r, offset)
          if !leftmost || m.offset(0).first < leftmost.first
            leftmost = m.offset(0)
            matched = m
            replace = replacements[i][2]
          end
        end
      }

      if !matched
        newline << line[offset..-1]
        break
      end

      nth += 1

      newline << line[offset...leftmost[0]]

      if nflag.empty? || nflag.include?(nth)
        reps += 1
        if rflag
          if replace == '\c'
            newline = nil
            break
          end

          newline << replace.gsub(/\\[1-9&]/) { |e|
            if e == '\&'
              matched[0]
            else
              matched[e[1].to_i]
            end
          }
        else
          newline << replace
        end
      else
        newline << line[offset...leftmost[1]]
      end

      if offset == leftmost[1]
        if offset == line.size
          break
        else
          newline << line[offset]
          offset += 1
          next
        end
      else
        offset = leftmost[1]
      end

      break  if offset >= line.size
    }

    if newline
      if nullflag
        newline << nl
      end

      if !pflag || nth > 0
        output.write newline
      end
    end
  
    break  if allflag
  end

  if cflag
    if (cflag.empty? && reps == 0) || !(cflag.empty? && !cflag.include?(reps))
      File.delete(tmpname)  if iflag
      output.close
      retval = 1
      next
    end
  end

  if iflag
    output.close
    if !iflag.empty?
      File.rename(file, file+iflag)
    end
    File.rename(tmpname, file)
  end
}

exit retval