Why and how to use File Handlers

From Scriptwiki
Jump to: navigation, search

File handlers are mIRC's most primitive method of handling file operations. If you are familiar with lower-level programing languages such as the popular C family and its derivatives, you will better recognize their functionality as that of "Streams". In these lower level languages, there are Input streams and Output streams (For reading from and writing to a file respectively), but mIRC uses file handling for both.

So why use file handlers when mIRC is a high level language and provides us with such useful and easy methods as the $read() identifier and write command? Consider the following code that reads from every line in the file with $read and a while loop.

alias example {
  var %i = 1
  while (%i <= $lines(examplefile.txt)) {
    noop $read(examplefile.txt,n,%i)
    inc %i
  }
}

When mIRC uses $read, it internally opens a "stream" with the file and goes through each line until it reaches the line it was told. Since we give it a number, it counts the lines. On the first round, mIRC reads line "1", sees we asked for line "1", and is done. On the second time however, it reads line "1", sees we asked for "2", keeps going, and sees line "2", its done. This will continue for line 3 (1 2 3) and 4 (1 2 3 4) until we are finished. If examplefile.txt were 50 lines big, mIRC would internally read 1275 lines before finishing. The formula for this if you're curious is: (1 + lines)*lines/2

So obviously there are performance issues with $read. If you think you will never have to do this, consider having to perform reverse wild card matches (wild card address in a file vs. full addresses, where you can't wild card search with $read), or parsing a downloaded web page.

Opening a Stream

A file handler stream must be opened when you want to begin, and closed when you are done. You will not be able to open two handlers of the same name without closing one first, nor will you be able to perform an operation on a stream you have not yet opened.

/fopen [-no] <Stream Name> <File Name>

This command opens a stream, assuming it is not opened already.

  • The -n switch will make this stream only work if the file doesn't exist, and will create it for you. If it does exist, the command will give an error.
  • The -o switch will create the file if it doesn't exist, and overwrite it if it does.

Without either of these switches, the command will give an error if the file does not exist, as it will be unable to create the stream.

Throughout this tutorial I will be using a file called bans.txt to illustrate my point. At the end, we will create an efficient alias ($isbanned(Address)) that will search the file line by line and see if any line wild card matches Address, returning $true or $false accordingly. I will build on it as we go, here is the first code to create the stream.

alias isbanned {
  if (!$isfile(bans.txt)) { return $false }
  .fopen bancheck bans.txt 
}

This opens a stream called "bancheck" with the file "bans.txt". The first line stops if "bans.txt" doesn't exist, because obviously the address can't be banned! The . in front of fopen is a "silent" command prefix, which keeps it from telling us about the file handler being opened. Most people don't care :)

Status of a Stream

$fopen has nothing inherent to do with opening the stream, its used to check the status of file handlers. After opening the handler in isbanned, we did not close it. At this point, if we were to echo $fopen(bancheck) it would return "bancheck", proving that the stream was opened. If it were not opened, it would return "$null". You can also use $fopen(N) to get file handler number N, 0 of course returning the total number of opened streams. $fopen(1), assuming this was the only file handler opened, would also return "bancheck", $fopen(0) would return 1.

Properties of $fopen:

  • fname returns the name of the file the stream is reading from. $fopen(bancheck).fname would return "bans.txt"
  • pos returns the current position of the file pointer. Think of the pointer as the blinking | on the screen when you type. As you read, the pointer advances to the next part of the file (after the last character you read). pos returns a number in bytes, basically the number of characters (letters, numbers, spaces) that have already been read. If the first line contained "foo bar" and we have read to "foo", .pos would return "3", as in three characters read. $fopen(bancheck).pos would return 0 as we have not read any bytes from the stream yet.
  • eof returns a $true or $false value based on whether or not the pointer has reached the end of the file (EoF is a general programing term for end of stream or, "End Of File"). You should note that if you intend to keep a stream open, .eof will bug as of version 6.21, because it is set to the number of bytes that were in the file when you began your read. If new text is written to the file while your pointer is opened (say from 30 to 50 bytes), once .pos was 30, .eof would be true, even though you were not technically at the end of the file. I recommend comparing .pos to $file(name).size instead of using if with eof. $fopen(bancheck).eof would return $false since we are not yet at the end of the file bans.txt
  • err simply returns an error code if there was one. $fopen(bancheck).err would return 0 because we have had no errors (hopefully).
alias isbanned {
  if (!$isfile(bans.txt)) { return $false }
  if ($fopen(bancheck)) { .fclose bancheck }
  .fopen bancheck bans.txt  
}

Here we add a check to make sure bancheck is not already opened for some reason, and close it if it is, avoiding the "name already in use" error.

Reading from the Stream

There are two ways to read from a stream. You can use $fread or $fgetc.

  • $fgetc(file handler name | file handler number) will return the next character after the current pointer position, and advanced the pointer past that character: assume the pointer is at the end of the bold characters: Before $fgetc: abc, After: abc
    • Returns: b, the next character in line.
  • $fread(name|number) will return the next line after the pointer, and advanced the pointer past that line.
  • $fread(name|number,M,&bvar) will read from the current pointer position, the next M bytes into &bvar
    • To learn about binary variables, see the Binary Files category.
alias isbanned { 
  if (!$isfile(bans.txt)) { return $false }
  if ($fopen(bancheck)) { .fclose bancheck }
  .fopen bancheck bans.txt  
  while (!$fopen(bancheck).eof) {
    var %thisAddress = $fread(bancheck)
  }
}

This would read every line in consecutive, non recursive order, setting the line to %thisAddress. Remember, without using $fread or $fgetc this loop would be infinite because the pointer would never advance and never reach the end of file (.eof)

Another way to do this is (more effective yet slower):

alias isbanned {
  if (!$isfile(bans.txt)) { return $false }
  if ($fopen(bancheck)) { .fclose bancheck }
  .fopen bancheck bans.txt
  while ($fopen(bancheck).pos < $file(bans.txt).size) {
    var %thisAddress = $fread(bancheck)
  }
}

This of course would read until the pointer is at the last position in the file (The last byte, its size in bytes).

Searching for Text in a Stream

File handlers support a bit of high-level influence, allowing you to search for a location in the stream and set the pointer to it using /fseek. Each seek is from the current position of the pointer foward.

/fseek [-lnwr] <Stream Name> <Pattern>
  • If you do not use a switch, it will look for the next occurance of the words you specify as pattern.
  • If you use -l, it will go to the Nth line (ie line 5, 5 as the pattern.)
  • If you use -n, it will seek to the next line automatically. This is fairly useless, in all honesty, but use as needed.
  • If you use -w, it will look by wild card as the pattern (*foobar*)
  • If you use -r, it will use a regular expression as the pattern (see http://www.regular-expressions.info or another tutorial to learn how to use these)

We might, for example, wish to start looking at the line that reads "Start Here", so we would use after opening the file:

.fseek bancheck Start Here

It is important to note that the pointer will be placed at the "S" in start here (The first byte in the found string), so to start reading after it, we should use:

noop $fread(bancheck)

This will read the next line (and do nothing with it), but will advanced the pointer past the line, letting us start after it.

Final Notes

  • fclose can use wild cards as well, .fclose * closes everything
  • If an error occurs, the handlers will not be closed and the reading will stop, do not forget to check to see if it is opened before trying to open it again, or you will have errors in your script.
  • $feof and $ferr are the values of .eof and .err for the last file handlers read from.
.flist [name | wildcard]
  • This will list all file handlers matching name or wild card; no parameter will list all file handlers.
.fwrite [-bn] <Stream Name> <Text | &Binvar>
  • This will write a binary (provided -b is specified), or text (-n will add an endline character) into the stream at the current position of the pointer.

Final Product

I'll leave you to analyze this and use what you have learned to understand it - now go and use $read ineffectively no more!

alias isbanned {
  if (!$isfile(bans.txt)) return $false
  if ($fopen(bancheck)) .fclose bancheck
  .fopen bancheck bans.txt
  if ($fopen(bancheck).err) return $false
  while ($fopen(bancheck).pos < $file(bans.txt).size) {
    var %thisAddress = $fread(bancheck)
    if (%thisAddress iswm $1) return $true
  }
  return $false
}

Example

A test alias showing speed differences of /fwrite & /write and $fread & $read

alias filetest {
  if ($isfile(filetest.txt)) { echo -ag The file filetest.txt already exists! | halt }
  if ($fopen(filetest)) { .fclose filetest }

  var %max = 1000, %str = $str(x,100)

  ;; WRITE-part

  var %x = %max, %t = $ticks
  .fopen -n filetest filetest.txt
  while (%x) {
    .fwrite -n filetest %str
    dec %x
  }
  .fclose filetest
  echo -ag /fwrite with %max lines: $calc($ticks - %t) $+ ms
  .remove filetest.txt

  var %x = %max, %t = $ticks
  while (%x) {
    write filetest.txt %str
    dec %x
  }
  echo -ag /write with %max lines: $calc($ticks - %t) $+ ms

  ;; READ-part

  var %t = $ticks
  .fopen filetest filetest.txt
  while (!$fopen(filetest).eof) {
    noop -qg $fread(filetest)
  }
  .fclose filetest
  echo -ag $ $+ fread with %max lines: $calc($ticks - %t) $+ ms

  var %x = %max,%t = $ticks
  while (%x) {
    noop $read(filetest.txt,%x)
    dec %x
  }
  echo -ag $ $+ read with %max lines: $calc($ticks - %t) $+ ms
  .remove filetest.txt
}

I got these results:

/fwrite with 5000 lines: 171ms
/write with 5000 lines: 1844ms
$fread with 5000 lines: 156ms
$read with 5000 lines: 16969ms
Contributed by aca20031