Saturday, August 21, 2010

Building "cat"

One neat feature of Factor is the ability to create and deploy programs as compiled binaries -- both CLI (command-line) or UI (graphical) applications.

I thought it might be fun to build the cat command-line program in Factor, and show how it can be deployed as a binary. From the man pages:

The cat utility reads files sequentially, writing them to the standard output. The file operands are processed in command-line order. If file is a single dash ('-') or absent, cat reads from the standard input.

We'll start by creating the cat vocabulary. You can either create the cat.factor file yourself, or use tools.scaffold to do it for you:

( scratchpad ) USE: tools.scaffold

( scratchpad ) "cat" scaffold-work
Creating scaffolding for P" resource:work/cat/cat.factor"

( scratchpad ) "cat" vocab edit

Begin the implementation by listing some imports and a namespace:

USING: command-line kernel io io.encodings.binary io.files
namespaces sequences strings ;

IN: cat

Printing each line from a stream is easy using the each-line word (flushing after each write to match the behavior of cat):

: cat-lines ( -- )
    [ write nl flush ] each-line ;

I chose to treat files (which might be text or binary) as binary, reading and writing 1024 bytes at a time. We check that the file exists, printing an error if not found:

: cat-stream ( -- )
    [ 1024 read dup ] [ >string write flush ] while drop ;

: cat-file ( path -- )
    dup exists?
    [ binary [ cat-stream ] with-file-reader ]
    [ write ": not found" write nl flush ] if ;

Given a list of files, with a special case for "-" (to read from standard input), we can cat each one:

: cat-files ( paths -- )
    [ dup "-" = [ drop cat-lines ] [ cat-file ] if ] each ;

Finally, we need an entry point that checks if command-line arguments have been provided:

: run-cat ( -- )
    command-line get [ cat-lines ] [ cat-files ] if-empty ;

MAIN: run-cat

Using the deploy-tool:

( scratchpad ) "cat" deploy-tool

Click "Save" to persist the deploy settings into a deploy.factor file, and "Deploy" to create a binary. You should see output like the following:

Deploying cat...
Writing vocabulary manifest
Preparing deployed libraries
Stripping manual memory management debug code
Stripping destructor debug code
Stripping stack effect checking from call( and execute(
Stripping specialized arrays
Stripping startup hooks
Stripping default methods
Stripping compiler classes
Finding megamorphic caches
Stripping globals
Compressing objects
Compressing quotations
Stripping word properties
Stripping symbolic word definitions
Stripping word names
Clearing megamorphic caches
Saving final image

And your binary should be in the same directory as your Factor installation (in a cat.app sub-directory on the Mac).

$ ls -hl cat.app/Contents/MacOS/cat 
-rwxr-xr-x  1 user  staff  421k Aug 21 11:11 cat.app/Contents/MacOS/cat*

$ cat.app/Contents/MacOS/cat
hello, world
hello, world
^D

The code for this is on my Github.

6 comments:

Guillermo said...

John, I really like your posts. Is there a way you could post about developing GUI? Or showing a simple PNG? I'm having trouble finding info online.

vegai said...

Does not quite work on 0.93:

The input quotations to "if" don't match their expected effects

Input
[ cat-lines ]

Expected
(( ..a -- ..b ))

Got
(( -- x ))

Input
[ cat-file ]

Expected
(( ..a -- ..b ))

Got

(( -- ))

mrjbq7 said...

Right, I was missing a "drop" in the post (was correct on Github). It's fixed now.

mrjbq7 said...

@Guillermo: Thanks! I plan on doing a few posts about GUI's soon. And I've got some image work coming up too.

Sam said...

Note that there is a race condition: you should not test for the existence of the file before opening it, but try to open the file and catch the error if there is one.

mrjbq7 said...

@Sam: Yep, a classic race condition. I thought it would be simpler to demonstrate with an existence check.

My original code used exception handling to trap the file system error as you suggest, but it seemed too advanced. Probably I should have kept it, though!