1 //==========================================================================
5 // Create ROM file system image
7 //==========================================================================
8 //####ECOSGPLCOPYRIGHTBEGIN####
9 // -------------------------------------------
10 // This file is part of eCos, the Embedded Configurable Operating System.
11 // Copyright (C) 1998, 1999, 2000, 2001, 2002 Red Hat, Inc.
13 // eCos is free software; you can redistribute it and/or modify it under
14 // the terms of the GNU General Public License as published by the Free
15 // Software Foundation; either version 2 or (at your option) any later version.
17 // eCos is distributed in the hope that it will be useful, but WITHOUT ANY
18 // WARRANTY; without even the implied warranty of MERCHANTABILITY or
19 // FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
22 // You should have received a copy of the GNU General Public License along
23 // with eCos; if not, write to the Free Software Foundation, Inc.,
24 // 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
26 // As a special exception, if other files instantiate templates or use macros
27 // or inline functions from this file, or you compile this file and link it
28 // with other works to produce a work based on this file, this file does not
29 // by itself cause the resulting work to be covered by the GNU General Public
30 // License. However the source code for this file must still be made available
31 // in accordance with section (3) of the GNU General Public License.
33 // This exception does not invalidate any other reasons why a work based on
34 // this file might be covered by the GNU General Public License.
36 // Alternative licenses for eCos may be arranged by contacting Red Hat, Inc.
37 // at http://sources.redhat.com/ecos/ecos-license/
38 // -------------------------------------------
39 //####ECOSGPLCOPYRIGHTEND####
40 //==========================================================================
41 //#####DESCRIPTIONBEGIN####
43 // Author(s): richard.panton@3glab.com
44 // Contributors: richard.panton@3glab.com
46 // Purpose: ROM file system
47 // Description: This program creates a ROM file system image, suitable
48 // for use with the sample ROM file system implemented by
50 // * CAUTION! * This is host code and can only be built
51 // in a host, e.g. Linux, environment.
53 //####DESCRIPTIONEND####
54 //==========================================================================
61 #include <sys/types.h>
68 //==========================================================================
70 // CONFIGURABLE ITEMS HERE
72 //==========================================================================
74 // define LONG to be a four byte unsigned integer on the host
77 // define SHORT to be a two byte unsigned integer on the host
78 #define SHORT uint16_t
80 // All data files should be aligned to this sized boundary (minimum probably 32)
83 // The data stored in a directory should be aligned to this size boundary
84 #define DIRECTORY_ALIGN 32
86 // All executable files should be aligned to this sized boundary (minimum probably 32)
89 // Undefine this if the host filesystem does not support lstat()
92 //==========================================================================
94 // Return (n) aligned to the next (b) byte boundary
95 #define ALIGN_TO( n, b ) (( (n) + (b)-1 ) & ~((b)-1))
97 // Set the stat call to use
99 #define get_status( p, b ) lstat( (p), (b) )
101 #define get_status( p, b ) stat( (p), (b) )
104 // This is how we identify a directory from its mode
105 #define IS_DIRECTORY( m ) (S_ISDIR(m))
107 // This is how we identify a data file from its mode
108 #define IS_DATAFILE( m ) (S_ISREG(m) && ((m)&S_IXUSR) == 0 )
110 // This is how we identify an executable from its mode
111 #define IS_EXECUTABLE( m ) (S_ISREG(m) && ((m)&S_IXUSR) != 0 )
113 // This is how we identify a symbolic link from its mode
114 #define IS_SYMLINK( m ) (S_ISLNK(m))
116 #define ROMFS_MAGIC 0x526f6d2e
118 //=========================================================================
120 #define EXIT_SUCCESS 0
122 #define EXIT_MALLOC 2
123 #define EXIT_FILESYS 3
126 #define EXIT_COMPILE 6
131 // These are the structures we will build into the ROMFS image.
132 // The sizes of these structures should be fixed for all architectures
133 typedef struct romfs_dirent {
136 char name[0]; // 8 + strlen(name) + 1
137 } romfs_dirent; // Aligns to next 32 byte boundary
139 typedef struct romfs_node {
146 LONG data_offset; // 24
148 } romfs_node; // Next node begins here
150 typedef struct romfs_disk {
156 } romfs_disk; // Nodes start here
158 // This is the holding structure for a node
159 typedef struct node {
160 const char *path; // Filename (inc. path) of a link to this node
161 size_t size; // Size of file/directory/link
162 mode_t st_mode; // Type and permissions
163 uid_t uid; // Owner id
164 gid_t gid; // Group id
165 time_t ctime; // File creation time
166 int nodenum; // Nodenumber of this node in the ROMFS image
167 dev_t device; // Device (for hardlink check)
168 ino_t inode; // Inode (for hardlink check)
169 int nlink; // [DIRECTORIES] Number of sub-directories [FILES] hard links
170 romfs_dirent *entry; // [DIRECTORIES] Points to an array of directory entries
171 int entry_size; // Size to be allocated to file (includes alignment bytes)
172 unsigned long offset; // Offset within ROMFS image of data
173 struct node *sibling; // Points to the next entry in this directory
174 struct node *child; // [DIRECTORIES] Points to any subdirectories
175 struct node *next_in_rom; // Next in ROMFS write order
176 struct node *next_multilink;// Next node that is multilinked
179 static int nodes = 0;
181 static int verbose = 1;
182 static int dowrite = 1;
183 static int bigendian = 0;
184 static int hardlinks = 0;
185 static unsigned long coffset = 0;
186 static node * first = NULL;
187 static node ** last_p = &first;
188 static node * first_multilink = NULL;
189 static node ** last_multilink_p = &first_multilink;
193 #define VERB_MINIMUM 1
196 #define VERB_EXCESSIVE 4
198 // Use gcc format argument checking on this function, which cannot return
199 static void fatal_error( int exitcode, const char *fmt, ... ) \
200 __attribute__ (( noreturn,format (printf, 2, 3) ));
202 // Use gcc format argument checking on this function
203 static void verb_printf( int level, const char *fmt, ... ) \
204 __attribute__ ((format (printf, 2, 3) ));
206 static void fatal_error( int exitcode, const char *fmt, ... ) {
210 vfprintf( stderr, fmt, v );
215 static void verb_printf( int level, const char *fmt, ... ){
216 if ( level <= verbose ) {
223 static void *mymalloc( size_t size ) {
224 void *p = malloc(size);
226 fatal_error( EXIT_MALLOC, "Out of memory allocating %d bytes\n", size );
231 static void myrealloc( void **o, size_t newsize ) {
233 *o = mymalloc( newsize );
234 else if ( !(*o = realloc( *o, newsize )) ) {
235 fatal_error( EXIT_MALLOC, "Out of memory re-allocating %d bytes\n", newsize );
239 static void outputlong( unsigned char *b, LONG w ) {
241 b[0] = (w>>24) & 0xff;
242 b[1] = (w>>16) & 0xff;
243 b[2] = (w>> 8) & 0xff;
246 b[3] = (w>>24) & 0xff;
247 b[2] = (w>>16) & 0xff;
248 b[1] = (w>> 8) & 0xff;
253 static void outputshort( unsigned char *b, SHORT w ) {
255 b[0] = (w>> 8) & 0xff;
258 b[1] = (w>> 8) & 0xff;
263 static unsigned long ConvertMode( unsigned long posix_mode ) {
264 unsigned long result = 0;
265 if ( S_ISDIR( posix_mode ) ) result |= 1<<0;
266 if ( S_ISCHR( posix_mode ) ) result |= 1<<1;
267 if ( S_ISBLK( posix_mode ) ) result |= 1<<2;
268 if ( S_ISREG( posix_mode ) ) result |= 1<<3;
269 if ( S_ISFIFO(posix_mode ) ) result |= 1<<4;
270 // We cannot create MQ, SEM, or SHM entries here
271 if ( posix_mode & S_IRUSR ) result |= 1<<16;
272 if ( posix_mode & S_IWUSR ) result |= 1<<17;
273 if ( posix_mode & S_IXUSR ) result |= 1<<18;
274 if ( posix_mode & S_IRGRP ) result |= 1<<19;
275 if ( posix_mode & S_IWGRP ) result |= 1<<20;
276 if ( posix_mode & S_IXGRP ) result |= 1<<21;
277 if ( posix_mode & S_IROTH ) result |= 1<<22;
278 if ( posix_mode & S_IWOTH ) result |= 1<<23;
279 if ( posix_mode & S_IXOTH ) result |= 1<<24;
280 if ( posix_mode & S_ISUID ) result |= 1<<25;
281 if ( posix_mode & S_ISGID ) result |= 1<<26;
285 static const char *AddDirEntry( const char *name, node *parent_node, int node_num ) {
286 int this_size = ((strlen(name) + 4 + 4 + 1) + 31) & ~31;
287 int start = parent_node->size;
289 myrealloc( (void**)&parent_node->entry, (parent_node->size += this_size) );
290 g = (romfs_dirent *)((unsigned char *)parent_node->entry + start);
291 memset( (void*)g, '\0', this_size );
292 outputlong( (unsigned char*)&g->node, node_num);
293 outputlong( (unsigned char*)&g->next, parent_node->size);
294 strcpy(g->name,name);
295 verb_printf( VERB_MAX, "\t%s --> node %d\n", name, node_num );
296 return (const char *)g->name;
301 static node * FindLink( dev_t d, ino_t i ) {
302 // See if the node has been previously included by checking the device/inode
303 // combinations of all known multi-linked nodes
304 node *np = first_multilink;
306 for ( ; np ; np = np->next_multilink ) {
307 if ( np->device == d && np->inode == i )
313 static node * GetNodeInfo( const char *path, const char *name, int *hlink ) {
318 sprintf(newpath,"%s/%s",path,name);
319 if ( (get_status(newpath,&stbuff)) < 0 ) {
320 fatal_error(EXIT_FILESYS, "stat(%s) failed: %s\n", newpath, strerror(errno));
322 if ( !(stbuff.st_mode & S_IRUSR) ) {
323 fatal_error(EXIT_FILESYS, "\"%s\" is not readable\n", newpath );
325 if ( hardlinks && S_ISREG( stbuff.st_mode ) && stbuff.st_nlink > 1 ) {
327 // See if this node has already been loaded
328 lnode = FindLink( stbuff.st_dev, stbuff.st_ino );
333 return lnode; // Return the found link instead
337 node = mymalloc( sizeof(struct node) );
339 // Incorporate the new link into the 'multi-linked' node list
340 *last_multilink_p = node;
341 last_multilink_p = &node->next_multilink;
344 node = mymalloc( sizeof(struct node) );
346 node->path = strdup( newpath );
347 // We re-calculate the size for directories
348 node->size = IS_DIRECTORY( stbuff.st_mode ) ? 0 : stbuff.st_size;
349 node->st_mode = stbuff.st_mode;
350 node->uid = stbuff.st_uid;
351 node->gid = stbuff.st_gid;
352 node->ctime = stbuff.st_ctime;
353 node->nodenum = nodes++;
354 node->device = stbuff.st_dev;
355 node->inode = stbuff.st_ino;
356 // We always re-calculate the number of links
357 node->nlink = IS_DIRECTORY( stbuff.st_mode ) ? 2 : 1;
359 node->entry_size = 0;
361 node->sibling = NULL;
363 node->next_in_rom = NULL;
364 node->next_multilink = NULL;
369 static void ScanDirectory(node *mynode, int p_node) {
373 node **last_p = &mynode->child;
377 if ( (dh = opendir( mynode->path )) == NULL ) {
378 perror(mynode->path);
382 verb_printf(VERB_EXCESSIVE, "Construct directory '%s'(%d):\n",
383 mynode->path, mynode->nodenum );
385 // Add . & .. here because they MUST be present in the image
386 AddDirEntry( ".", mynode, mynode->nodenum );
387 AddDirEntry( "..", mynode, p_node );
389 while ( (e = readdir( dh )) ) {
390 // Ignore . & .. here because they MAY NOT be in the host filesystem
391 if ( strcmp(e->d_name,".") && strcmp(e->d_name,"..") ) {
394 th = GetNodeInfo( mynode->path, e->d_name, &was_hardlinked );
395 AddDirEntry( e->d_name, mynode, th->nodenum );
397 if ( !was_hardlinked ) {
398 verb_printf( VERB_EXCESSIVE, "\t\tNew node %d for entry '%s'\n", th->nodenum, e->d_name);
400 last_p = &th->sibling;
402 verb_printf( VERB_EXCESSIVE, "\t\tRe-used node %d for entry '%s'\n", th->nodenum, e->d_name);
407 verb_printf(VERB_EXCESSIVE,"Completed '%s'. Checking for child directories...\n", mynode->path);
409 for ( th = mynode->child ; th ; th = th->sibling ) {
410 if ( IS_DIRECTORY( th->st_mode ) ) {
412 ScanDirectory( th, mynode->nodenum );
417 static void AllocateSpaceToDirectories( node *first ) {
420 for ( np = first ; np ; np = np->sibling ) {
421 if ( IS_DIRECTORY( np->st_mode ) ) {
422 // The first node is a directory. Add its data
423 np->offset = coffset;
424 np->entry_size = ALIGN_TO( np->size, DIRECTORY_ALIGN );
425 coffset += np->entry_size;
427 verb_printf( VERB_MAX, "\t\tnode %5d : 0x%06lX (+0x%05X)\n",
428 np->nodenum, np->offset, np->entry_size );
430 // Link this node into the write order chain.
431 // For node 0 (the root), this will overwrite the first pointer with itself
433 last_p = &np->next_in_rom;
437 // Now add any child directories
438 for ( np = first ; np ; np = np->sibling ) {
439 if ( IS_DIRECTORY( np->st_mode ) && np->child )
440 AllocateSpaceToDirectories( np->child );
444 static void AllocateSpaceToDataFiles( node *first ) {
447 // There are two loops below. It CAN be done in just one, but this re-orders
448 // the file positions in relation to their inode numbers. To keep it simple
449 // to check, allocation takes place in the first loop, recursion in the second
451 // Search for child data files
452 for ( np = first->child ; np ; np = np->sibling ) {
453 if ( IS_DATAFILE( np->st_mode ) || IS_SYMLINK( np->st_mode ) ) {
454 np->offset = coffset;
455 np->entry_size = ALIGN_TO( np->size, DATA_ALIGN );
456 coffset += np->entry_size;
458 // Link in to the rom write order list
460 last_p = &np->next_in_rom;
462 verb_printf( VERB_MAX, "\t\tnode %5d : 0x%06lX (+0x%05X)\n",
463 np->nodenum, np->offset, np->entry_size );
467 // Recurse into sub-directories
468 for ( np = first->child ; np ; np = np->sibling ) {
469 if ( IS_DIRECTORY( np->st_mode ) ) {
470 AllocateSpaceToDataFiles( np );
475 static void AllocateSpaceToExecutables( node *first ) {
478 // The first node is a directory. Don't bother with that...
480 // Search for child executables
481 for ( np = first->child ; np ; np = np->sibling ) {
482 if ( IS_EXECUTABLE( np->st_mode ) ) {
483 np->offset = coffset;
484 np->entry_size = ALIGN_TO( np->size, EXEC_ALIGN );
485 coffset += np->entry_size;
487 // Link in to the rom write order list
489 last_p = &np->next_in_rom;
491 verb_printf( VERB_MAX, "\t\tnode %5d : 0x%06lX (+0x%05X)\n",
492 np->nodenum, np->offset, np->entry_size );
496 // Recurse into sub-directories
497 for ( np = first->child ; np ; np = np->sibling ) {
498 if ( IS_DIRECTORY( np->st_mode ) ) {
499 AllocateSpaceToExecutables( np );
504 static void WriteNode( int fd, node *np ) {
507 outputlong( (unsigned char*) &anode.mode, ConvertMode( np->st_mode ) );
508 outputlong( (unsigned char*) &anode.nlink, np->nlink );
509 outputshort((unsigned char*) &anode.uid, np->uid );
510 outputshort((unsigned char*) &anode.gid, np->gid );
511 outputlong( (unsigned char*) &anode.size, np->size );
512 outputlong( (unsigned char*) &anode.ctime, np->ctime );
513 outputlong( (unsigned char*) &anode.data_offset, np->offset );
514 sprintf( padhere, "<%6d>", np->nodenum );
515 memcpy( anode.pad, padhere, 8 );
516 if ( dowrite && write( fd, (void*)&anode, sizeof(anode) ) != sizeof(anode) )
517 fatal_error(EXIT_WRITE, "Error writing node %d (%s): %s\n", np->nodenum, np->path, strerror(errno) );
520 static int WriteNodeAndSiblings( int fd, int nodenum, node *first ) {
523 for ( np = first ; np ; np = np->sibling ) {
524 if ( np->nodenum != nodenum++ ) {
525 fatal_error(EXIT_BUG, "BUG: Out of sequence node number; got %d, expected %d\n", np->nodenum, nodenum-1);
530 for ( np = first ; np ; np = np->sibling ) {
531 if ( IS_DIRECTORY( np->st_mode ) && np->child ) {
532 nodenum = WriteNodeAndSiblings( fd, nodenum, np->child );
538 static void WriteNodeTable( int fd ) {
542 outputlong( (unsigned char*) &header.magic, ROMFS_MAGIC );
543 outputlong( (unsigned char*) &header.nodecount, nodes );
544 outputlong( (unsigned char*) &header.disksize, coffset );
545 outputlong( (unsigned char*) &header.dev_id, 0x01020304 );
546 strcpy( header.name, "ROMFS v1.0" );
547 if ( dowrite && write( fd, (void*)&header, sizeof(header) ) != sizeof(header) )
548 fatal_error(EXIT_WRITE, "Error writing ROMFS header: %s\n", strerror(errno) );
550 if ( (wnodes = WriteNodeAndSiblings( fd, 0, first )) != nodes ) {
551 fatal_error(EXIT_BUG, "BUG: Lost/gained some nodes; wrote %d, expected %d\n", wnodes, nodes );
559 static void WriteData( int fd, node *np ) {
564 if ( IS_SYMLINK( np->st_mode ) ) {
565 if ( (ffd = readlink( np->path, newpath, sizeof(newpath) )) < 0 )
566 fatal_error(EXIT_FILESYS, "Error reading symlink \"%s\": %s\n", np->path, strerror(errno) );
568 if ( !dowrite ) return;
570 if ( lseek( fd, np->offset, SEEK_SET ) != np->offset )
571 fatal_error(EXIT_SEEK, "Error seeking to offset 0x%lX: %s\n", np->offset, strerror(errno) );
573 if ( write( fd, newpath, ffd ) != ffd )
574 fatal_error(EXIT_WRITE, "Write error: %s\n", strerror(errno) );
579 if ( (ffd=open(np->path, O_RDONLY | O_BINARY )) < 0 )
580 fatal_error(EXIT_FILESYS, "Error opening \"%s\": %s\n", np->path, strerror(errno) );
582 if ( dowrite && lseek( fd, np->offset, SEEK_SET ) != np->offset )
583 fatal_error(EXIT_SEEK, "Error seeking to offset 0x%lX: %s\n", np->offset, strerror(errno) );
586 while ( todo >= 1024 ) {
587 if ( read( ffd, newpath, 1024 ) != 1024 )
588 fatal_error(EXIT_FILESYS, "Error reading file \"%s\" at offset 0x%lX: %s\n", np->path, np->size - todo, strerror(errno) );
589 if ( dowrite && write( fd, newpath, 1024 ) != 1024 )
590 fatal_error(EXIT_WRITE, "Write error: %s\n", strerror(errno) );
595 if ( read( ffd, newpath, todo ) != todo )
596 fatal_error(EXIT_FILESYS, "Error reading file \"%s\" at offset 0x%lX: %s\n", np->path, np->size - todo, strerror(errno) );
597 if ( dowrite && write( fd, newpath, todo ) != todo )
598 fatal_error(EXIT_WRITE, "Write error: %s\n", strerror(errno) );
605 static void WriteDataBlocks( int fd, node *first ) {
606 for ( ; first ; first = first->next_in_rom ) {
607 if ( dowrite && lseek( fd, first->offset, SEEK_SET ) != first->offset )
608 fatal_error(EXIT_SEEK, "Error seeking to offset 0x%lX: %s\n", first->offset, strerror(errno) );
609 if ( IS_DIRECTORY( first->st_mode ) ) {
610 if ( dowrite && write( fd, first->entry, first->size ) != first->size )
611 fatal_error(EXIT_WRITE, "Write error: %s\n", strerror(errno) );
613 WriteData( fd, first );
618 static void usage(void) {
619 fprintf(stderr,"\n%s - Create an eCos ROMFS disk image from the files\n",prog);
620 fprintf(stderr,"%*s contained under a specified directory\n\n", strlen(prog), "");
621 fprintf(stderr,"Usage: %s [options] <fs_root> <fs_file>\n", prog);
622 fprintf(stderr," fs_root is the directory containing the files to package into the ROMFS image\n");
623 fprintf(stderr," fs_file is the name of the ROMFS image file to create\n");
624 fprintf(stderr," Options include:\n");
625 fprintf(stderr," -v / -q increase / decrease verbosity\n");
626 fprintf(stderr," -n do everything EXCEPT creating the output file\n");
627 fprintf(stderr," -b write a big-endian image (default is little endian)\n");
628 fprintf(stderr," -l collapse hard links to a single node\n");
629 fprintf(stderr,"\n");
633 int main(int ac, char *av[]) {
638 // Check structure sizes
639 if (sizeof(romfs_node) != 32) {
640 fatal_error(EXIT_COMPILE , "Size of romfs_node is %d, NOT 32\n", sizeof(romfs_node) );
641 } else if (sizeof(romfs_dirent) != 8) {
642 fatal_error(EXIT_COMPILE , "Size of romfs_dirent is %d, NOT 8\n", sizeof(romfs_dirent) );
643 } else if (sizeof(romfs_disk) != 32) {
644 fatal_error(EXIT_COMPILE , "Size of romfs_disk is %d, NOT 32\n", sizeof(romfs_disk) );
647 // Parse option arguments
648 while ( ac > 1 && av[1][0] == '-' ) {
668 fprintf(stderr,"%s: Invalid flag -%c\n", prog, *o );
675 // Check remaining arguments
676 if ( ac != 3 ) usage();
679 verb_printf( VERB_MINIMUM, "%s: Verbosity %d %s%s endian\n",
681 dowrite ? "" : "no write, ",
682 bigendian ? "big" : "little" );
684 // Phase 1. Recursively scan the root directory for files and directories.
685 verb_printf(VERB_MINIMUM, "Phase 1 - Build file list\n");
687 first = GetNodeInfo( av[1], ".", &dummy ); // Initialize the root node entry.
688 ScanDirectory( first, 0 );
690 // Phase 2. Work out space allocations for filesystem
691 verb_printf(VERB_MINIMUM, "Phase 2 - Calculate space allocation\n");
692 coffset = sizeof(romfs_disk) + nodes * sizeof(romfs_node);
693 verb_printf(VERB_MAX,"\t\tnode table : 0x000000 (+0x%05lX) %d nodes\n", coffset, nodes );
695 // Phase 2a. Work out space allocations for the directories of the filesystem
696 verb_printf(VERB_SUB,"Phase 2a - * Directories\n");
697 coffset = ALIGN_TO( coffset, DIRECTORY_ALIGN );
698 AllocateSpaceToDirectories( first );
700 // Phase 2b. Work out space allocations for the data files of the filesystem
701 verb_printf(VERB_SUB,"Phase 2b - * Regular files\n");
702 coffset = ALIGN_TO( coffset, DATA_ALIGN );
703 AllocateSpaceToDataFiles( first );
705 // Phase 2c. Work out space allocations for the executable files of the filesystem
706 verb_printf(VERB_SUB,"Phase 2c - * Executable files\n");
707 coffset = ALIGN_TO( coffset, EXEC_ALIGN );
708 AllocateSpaceToExecutables( first );
710 // Round off the image size...
711 coffset = ALIGN_TO( coffset, EXEC_ALIGN );
713 // Phase 3. Write out the image file
714 verb_printf(VERB_MINIMUM, "Phase 3 - Construct ROMFS image file (%ld kb)\n", ALIGN_TO( coffset, 1024 )/1024);
717 if ( (fd = open( av[2], O_WRONLY|O_CREAT|O_TRUNC|O_BINARY, 0666 )) < 0 ) {
718 fatal_error(EXIT_WRITE,"Failed to open output file '%s', errno=%d\n", av[2], errno );
721 verb_printf(VERB_NONE," (No image is being written)\n");
724 verb_printf(VERB_SUB,"Phase 3a - * Node table\n");
725 WriteNodeTable( fd );
727 verb_printf(VERB_SUB,"Phase 3b - * Data blocks\n");
728 WriteDataBlocks( fd, first );
730 if ( fd >= 0 ) close(fd);
732 verb_printf(VERB_MINIMUM, "%s completed\n", av[2] );