1.4.1.13. fejezet, File modul és IO

A Ruby fájl kezelésre egy IO nevű osztályt jelölt ki, ami képes megnyitni és lezárni adatfolyamokat (streams), bájtok sorozatát, majd írhatjuk és olvashatjuk ezeket az osztály metódusaival. Például van egy textfile.txt állományunk, néhány sor szöveggel. Így nyitható meg a fájl, és olvasható be a szöveg:

IO.foreach("testfile.txt") {|line| print( line ) }

Itt a foreach az IO osztály metódusa, vagyis nem szükséges példányosítani az IO-t hogy használjuk, csak egy fájlnevet kell átadni, és a foreach kap egy blokkot, amivel az összes sort feldolgozza. Nem kell megnyitni majd lezárni a fájlt, mint más programnyelveknél, a Ruby.IO.foreach megteszi ezeket nekünk.

Az IO-nak rengeteg ehhez hasonló metódusa van. Például itt a readlines metódus, ami tömbbe olvassa a fájlt későbbi használatra:

lines = IO.readlines("testfile.txt")
lines.each{|line| print( line )}

A File osztály egy leszármazottja az IO osztálynak, és a fenti eljárások átalakíthatók felhasználva a File osztályt:

File.foreach("testfile.txt") {|line| print( line ) } 
lines = File.readlines("testfile.txt")
lines.each{|line| print( line )}

Fájl megnyitása és lezárása

Egy állomány a File példányosításán keresztül, a new metódussal, vagy az open metódussal nyitható meg. Első paramétere a fájl neve, második a megnyitás módja.

Mód Jelentés
"r" Csak olvasásra, kezdés a file elején (alapértelmezett mód).
"r+" Írásra-olvasásra, kezdés a file elején
"w" Csak írásra, levágja a file-t nulla hosszúságra, vagy létrehoz egy új fájlt írásra.
"w+" Írásra-olvasásra, levágja a file-t nulla hosszúságra, vagy létrehoz egy új fájlt írásra-olvasásra.
"a" Csak írásra, kezdés a fájl végén, ha létezik, különben létrehoz egy új fájlt írásra
"a+" Írásra-olvasásra, kezdés a fájl végén, ha létezik, különben létrehoz egy új fájlt írásra-olvasásra
"b" (csak DOS/Windows) Bináris file mód (együtt használható a fenti karakterek közül valamelyikkel).

Nézzünk egy példát egy teljes feldolgozásra.

f = File.new("myfile.txt", "w") 
f.puts( "I", "wandered", "lonely", "as", "a", "cloud" ) 
f.close

A lezárás nem csak a fájl mutató elengedését jelenti, hanem a buffer kiírását is. Most, hogy kiírtunk néhány sort, olvassuk vissza. Most történjen egy kis számolás is, számoljuk meg a karaktereket (ASCII fájlba egy bájt egy karakter), és a sorokat (újsor karakter az ASCII 10, amit a "\n" helyére kell írni, ha 1.8-as Ruby-val dolgozunk. Ebben az esetben ugyanis a getc() metódus FixNum típussal tér vissza).

charcount = 0
linecount = 0
 
f = File.new("myfile.txt", "r") 
while !( f.eof ) do
  c = f.getc()
  if ( ((c.class == String)&&(c == "\n")) || ((c.class == Fixnum)&&(c==10)) ) then
     linecount += 1
     puts( " <End Of Line #{linecount}>" )
   else
    putc( c )
    charcount += 1
  end
end
 
if f.eof then 
  puts( "<End Of File>" ) 
end
 
f.close
 
puts("This file contains #{linecount} lines and #{charcount} characters." )

Eredmények (1.9-es Ruby-val):

I <End Of Line 1>
wandered <End Of Line 2>
lonely <End Of Line 3>
as <End Of Line 4>
a <End Of Line 5>
cloud <End Of Line 6>
<End Of File>
This file contains 6 lines and 23 characters.

Fájlok és könyvtárak

A fájl vagy könyvtár létezését a File.exist? metódussal ellenőrizhetjük.

if File.exist?( "C:\\" ) then
  puts( "Yup, you have a C:\\ directory" ) 
else 
  puts( "Eeek! Can't find the C:\\ drive!" ) 
end

Ha különbséget szeretnél tenni adatállomány és könyvtár között, használt a directory? metódust.

def dirOrFile( aName ) 
  if File.directory?( aName ) then 
    puts( "#{aName} is a directory" ) 
  else 
    puts( "#{aName} is a file" ) 
  end
end

Fájlok másolása

Másoljunk most egy könyvtárból fájlokat.

require( "fileutils" )
 
overwrite_prompt = true
 
# get source directory
puts( "FROM which directory would you like to copy the files?" )
sourcedir = gets().chomp()
 
if !(File.directory?(sourcedir)) then
  puts( "A directory called #{sourcedir} cannot be found!" )
else
# get target dir
  puts( "INTO which directory would you like to copy the files?" )
  targetdir = gets().chomp()
 
  ok = true	# if targetdir doesn't exist...
  if !(File.directory?(targetdir) ) then
    ok = false
    puts( "#{targetdir} cannot be found!" )
    puts( "Would you like to create it?")
    answer = gets()
    if (answer[0,1].downcase == 'y') then
      FileUtils.mkdir( targetdir ) # create targetdir
      ok = true
    end
  end
  if ok then
    Dir.foreach( sourcedir ){
      |f|
      filepath = "#{sourcedir}#{File::SEPARATOR}#{f}"
      if !(File.directory?(filepath) ) then  	
        if File.exist?("#{targetdir}#{File::SEPARATOR}#{f}") then
	  puts("#{f} already exists in target directory (not copied)" )
        else
	  FileUtils.cp( filepath, targetdir )
	  puts("Copying... #{filepath}" )
        end
      end
    }
  end
end # if sourcedir was not found
puts( "End" )

Ez a program nem másolja az egész könyvtár struktúrát, csak a megadott könyvtárat. Hiba a fenti programban, hogy fájl és könyvtár neveket nem alakítja át a rendszer számára biztonságos formára. Így a balra dőlő dupla perjel a könyvtárnév elválasztásánál kivételt generált Linuxon, ezért javítottam a mintaprogramba. A hozzáférhetőségi jogosultságokat sem ellenőrzi, ami szintén kivételt generál. Ez még TODO szakaszban van, legalább is a könyvben. Az interneten több helyen egy Bash script hívást egyszerűbb megoldásnak tartanak. Később látunk majd rekurzív eljárást, ami az egész könyvtár hierarchiát másolja.

Könyvtár tudakozó

Most, hogy másoltunk egy könyvtárat, nézzük meg egy másik által foglalt méretet. Ehhez le kell menni minden alsó könyvtárakba, és azok összes alkönyvtáraiba, és így tovább. Ezt rekurzióval csináljuk.

$dirsize = 0 # total size of all files located in entire directory tree
 
def processfiles( aDir )
  totalbytes = 0
  Dir.foreach( aDir ){ # process all files in a directory
    |f|
    mypath = "#{aDir}#{File::SEPARATOR}#{f}"
    s = ""
    if File.directory?(mypath) then
      if f != '.' and f != '..' then
        bytes_in_dir = processfiles(mypath)
        puts( "<DIR> ---> #{mypath} contains [#{bytes_in_dir/1024}] KB" )
      end
    else
      filesize = File.size(mypath)
      totalbytes += filesize
      puts ( "#{mypath} : #{filesize/1024}K" )
    end
  }
end
 
dirname = ".." # <= initially this is set to the parent of current directory
 
if !(File.directory?(dirname)) then
  puts( "#{dirname} is not a valid directory" )
else
  processfiles( dirname ) # <= This is where processfiles is first called
  printf( "Size of this directory and subdirectories is #{$dirsize} bytes, #{$dirsize/1024}K, %0.02fMB", "#{$dirsize/1048576.0}" )
end	

Itt is a szóközök és a hozzáférési jogosultság okozhat kivételeket.

Sorba rendezés méret alapján

A fenti program névsorban adja vissza a könyvtárak és fájlok méretét, most viszont a fájlokat relatív mérete alapján rendezzük sorba, csökkenő méret szerint.

$dirsize = 0
$files = []
 
def processfiles( aDir )
 totalbytes = 0
 Dir.foreach( aDir ){
   |f|
   mypath = "#{aDir}#{File::SEPARATOR}#{f}"
   if File.directory?(mypath) then
     if f != '.' and f != '..' then
       fsize = processfiles(mypath) / 1024	
       puts( "<DIR> --->#{mypath} contains [#{fsize}] KB" )
       $files << fsize
     end
   else
     filesize = File.size(mypath)
     totalbytes += filesize
   end
 }
 
 $dirsize += totalbytes
 return totalbytes
end
 
 
dirname = ".."
 
if !(File.directory?(dirname)) then
  puts( "#{dirname} is not a valid directory" )
else
  processfiles( dirname )	
  printf("Size of #{dirname} and subdirectories is #{$dirsize} bytes, #{$dirsize/1024}K, %0.02fMB\n\n",  "#{$dirsize/1048576.0}" )
  puts( "This is an unordered list of the file sizes..." )
  p( $files )
  puts( "This is an unordered list of the file sizes (with 0 byte entries deleted)..." )
  $files.delete(0) 
  p( $files )
  puts( "This is a sorted list (low to high) of the file sizes" )
  p( $files.sort )
  puts( "This is a sorted list (high to low) of the file sizes" )
  p( $files.sort.reverse )
end

Az egyetlen baj a fenti megoldással, hogy megvan ugyan a fájlok mérete, viszont a hozzá tartozó fájl neve hiányzik. Egy jobb megoldás a Hash tömbök használata tömbök helyett.

$dirsize = 0
$dirs = {}
$files = {}
 
def processfiles( aDir )
 totalbytes = 0
 Dir.foreach( aDir ){
   |f|
   mypath = "#{aDir}#{File::SEPARATOR}#{f}"
   if File.directory?(mypath) then	# if this is a directory
     if f != '.' and f != '..' then	# recurse...
       dsize = processfiles(mypath) / 1024	
       $dirs[mypath] = dsize
     end
   else
     filesize = File.size(mypath)
     totalbytes += filesize
     $files[mypath] = filesize
   end
  }
  $dirsize += totalbytes
  return totalbytes
end
 
 
dirname = ".."
 
 
if !(File.directory?(dirname)) then
  puts( "#{dirname} is not a valid directory" )
else
  processfiles( dirname )
  printf("Size of this directory and subdirectories is #{$dirsize} bytes, #{$dirsize/1024}K, %0.02fMB\n\n",  "#{$dirsize/1048576.0}" )
  puts( "File sizes (ascending)...")	
  $files = $files.sort{|a,b| a[1]<=>b[1]} 
  $files.each{ |fname,fsize| puts( "#{fname} : #{fsize} bytes" ) }
  puts( "\nDirectory sizes (ascending)...")
  $dirs = $dirs.sort{|a,b| a[1]<=>b[1]} 
  $dirs.each{ |dname,dsize| puts( "#{dname} : #{dsize}K" ) }
end