Commit | Line | Data |
---|---|---|
805e021f CE |
1 | /* |
2 | * Copyright 2000, International Business Machines Corporation and others. | |
3 | * All Rights Reserved. | |
4 | * | |
5 | * This software has been released under the terms of the IBM Public | |
6 | * License. For details, see the LICENSE file in the top-level source | |
7 | * directory or online at http://www.openafs.org/dl/license10.html | |
8 | */ | |
9 | ||
10 | #include <afsconfig.h> | |
11 | #include <afs/param.h> | |
12 | ||
13 | #include <roken.h> | |
14 | ||
15 | #include <afs/cmd.h> | |
16 | #include <lock.h> | |
17 | #include <afs/tcdata.h> | |
18 | #include <afs/usd.h> | |
19 | ||
20 | usd_handle_t fd; | |
21 | usd_handle_t ofd; | |
22 | int ofdIsOpen = 0; | |
23 | afs_int32 nrestore, nskip; | |
24 | int ask, printlabels, printheaders, verbose; | |
25 | char filename[100]; | |
26 | char *outfile; | |
27 | ||
28 | #define TAPE_MAGIC 1100000009 /* Defined in file_tm.c */ | |
29 | #define BLOCK_MAGIC 1100000005 | |
30 | #define FILE_MAGIC 1000000007 | |
31 | #define FILE_BEGIN 0 | |
32 | #define FILE_END 1 | |
33 | #define FILE_EOD -1 | |
34 | ||
35 | struct tapeLabel { /* also in file_tm.c */ | |
36 | afs_int32 magic; | |
37 | struct butm_tapeLabel label; | |
38 | }; | |
39 | struct fileMark { /* also in file_tm.c */ | |
40 | afs_int32 magic; | |
41 | afs_uint32 nBytes; | |
42 | }; | |
43 | ||
44 | /* Read a tape block of size 16K */ | |
45 | afs_int32 | |
46 | readblock(char *buffer) | |
47 | { | |
48 | u_int nread, total = 0; | |
49 | int rc, fmcount = 0; | |
50 | ||
51 | while (total < BUTM_BLOCKSIZE) { | |
52 | rc = USD_READ(fd, buffer + total, BUTM_BLOCKSIZE - total, &nread); | |
53 | if (rc != 0) { | |
54 | return (rc); | |
55 | } else if ((nread == 0) && (total == 0)) { | |
56 | if (verbose) | |
57 | fprintf(stderr, "Hardware file mark\n"); | |
58 | if (++fmcount > 3) { | |
59 | if (verbose) | |
60 | fprintf(stderr, | |
61 | "Greater than 3 hardware file marks in a row - done\n"); | |
62 | return -1; | |
63 | } | |
64 | } else if (nread == 0) { | |
65 | fprintf(stderr, "Reached unexpected end of dump file\n"); | |
66 | return -1; | |
67 | } else { | |
68 | total += nread; | |
69 | } | |
70 | } | |
71 | return 0; | |
72 | } | |
73 | ||
74 | void | |
75 | printLabel(struct tapeLabel *tapeLabelPtr) | |
76 | { | |
77 | tapeLabelPtr->label.dumpid = ntohl(tapeLabelPtr->label.dumpid); | |
78 | tapeLabelPtr->label.creationTime = | |
79 | ntohl(tapeLabelPtr->label.creationTime); | |
80 | tapeLabelPtr->label.expirationDate = | |
81 | ntohl(tapeLabelPtr->label.expirationDate); | |
82 | tapeLabelPtr->label.structVersion = | |
83 | ntohl(tapeLabelPtr->label.structVersion); | |
84 | tapeLabelPtr->label.useCount = ntohl(tapeLabelPtr->label.useCount); | |
85 | tapeLabelPtr->label.size = ntohl(tapeLabelPtr->label.size); | |
86 | ||
87 | fprintf(stderr, "\nDUMP %u\n", tapeLabelPtr->label.dumpid); | |
88 | if (printlabels) { | |
89 | time_t t; | |
90 | ||
91 | fprintf(stderr, " AFS Tape Name : %s\n", | |
92 | tapeLabelPtr->label.AFSName); | |
93 | fprintf(stderr, " Permanent Name : %s\n", | |
94 | tapeLabelPtr->label.pName); | |
95 | fprintf(stderr, " Dump Id : %u\n", | |
96 | tapeLabelPtr->label.dumpid); | |
97 | t = tapeLabelPtr->label.dumpid; | |
98 | fprintf(stderr, " Dump Id Time : %.24s\n", | |
99 | ctime(&t)); | |
100 | t = tapeLabelPtr->label.creationTime; | |
101 | fprintf(stderr, " Date Created : %.24s\n", | |
102 | ctime(&t)); | |
103 | t = tapeLabelPtr->label.expirationDate; | |
104 | fprintf(stderr, " Date Expires : %.24s\n", | |
105 | ctime(&t)); | |
106 | fprintf(stderr, " Version Number : %d\n", | |
107 | tapeLabelPtr->label.structVersion); | |
108 | fprintf(stderr, " Tape Use Count : %d\n", | |
109 | tapeLabelPtr->label.useCount); | |
110 | fprintf(stderr, " Tape Size : %u\n", | |
111 | tapeLabelPtr->label.size); | |
112 | fprintf(stderr, " Comment : %s\n", | |
113 | tapeLabelPtr->label.comment); | |
114 | fprintf(stderr, " Dump Path : %s\n", | |
115 | tapeLabelPtr->label.dumpPath); | |
116 | fprintf(stderr, " Cell Name : %s\n", | |
117 | tapeLabelPtr->label.cell); | |
118 | fprintf(stderr, " Creator Name : %s\n", | |
119 | tapeLabelPtr->label.creator.name); | |
120 | fprintf(stderr, " Creator Instance : %s\n", | |
121 | tapeLabelPtr->label.creator.instance); | |
122 | fprintf(stderr, " Creator Cell : %s\n", | |
123 | tapeLabelPtr->label.creator.cell); | |
124 | } | |
125 | } | |
126 | ||
127 | void | |
128 | printHeader(struct volumeHeader *headerPtr, afs_int32 *isvolheader) | |
129 | { | |
130 | static int volcount = 0; | |
131 | ||
132 | *isvolheader = 0; | |
133 | headerPtr->volumeID = ntohl(headerPtr->volumeID); | |
134 | headerPtr->server = ntohl(headerPtr->server); | |
135 | headerPtr->part = ntohl(headerPtr->part); | |
136 | headerPtr->from = ntohl(headerPtr->from); | |
137 | headerPtr->frag = ntohl(headerPtr->frag); | |
138 | headerPtr->magic = ntohl(headerPtr->magic); | |
139 | headerPtr->contd = ntohl(headerPtr->contd); | |
140 | headerPtr->dumpID = ntohl(headerPtr->dumpID); | |
141 | headerPtr->level = ntohl(headerPtr->level); | |
142 | headerPtr->parentID = ntohl(headerPtr->parentID); | |
143 | headerPtr->endTime = ntohl(headerPtr->endTime); | |
144 | headerPtr->versionflags = ntohl(headerPtr->versionflags); | |
145 | headerPtr->cloneDate = ntohl(headerPtr->cloneDate); | |
146 | ||
147 | if (headerPtr->magic == TC_VOLBEGINMAGIC) { | |
148 | time_t t; | |
149 | ||
150 | *isvolheader = 1; | |
151 | if (verbose) | |
152 | fprintf(stderr, "Volume header\n"); | |
153 | t = headerPtr->from; | |
154 | fprintf(stderr, | |
155 | "VOLUME %3d %s (%u) - %s dump from %.24s", | |
156 | ++volcount, headerPtr->volumeName, headerPtr->volumeID, | |
157 | (headerPtr->level ? "Incr" : "Full"), | |
158 | (t ? (char *)ctime(&t) : "0")); | |
159 | /* do not include two ctime() calls in the same fprintf call as | |
160 | * the same string buffer will be returned by each call. */ | |
161 | t = headerPtr->cloneDate; | |
162 | fprintf(stderr, " till %.24s\n", ctime(&t)); | |
163 | if (printheaders) { | |
164 | fprintf(stderr, " Volume Name = %s\n", | |
165 | headerPtr->volumeName); | |
166 | fprintf(stderr, " Volume ID = %u\n", headerPtr->volumeID); | |
167 | t = headerPtr->cloneDate; | |
168 | fprintf(stderr, " Clone Date = %.24s\n", | |
169 | ctime(&t)); | |
170 | fprintf(stderr, " Vol Fragment = %d\n", headerPtr->frag); | |
171 | fprintf(stderr, " Vol Continued = 0x%x\n", headerPtr->contd); | |
172 | fprintf(stderr, " DumpSet Name = %s\n", | |
173 | headerPtr->dumpSetName); | |
174 | fprintf(stderr, " Dump ID = %u\n", headerPtr->dumpID); | |
175 | fprintf(stderr, " Dump Level = %d\n", headerPtr->level); | |
176 | t = headerPtr->from; | |
177 | fprintf(stderr, " Dump Since = %.24s\n", | |
178 | ctime(&t)); | |
179 | fprintf(stderr, " parent Dump ID = %u\n", headerPtr->parentID); | |
180 | } | |
181 | } else if (headerPtr->magic == TC_VOLENDMAGIC) { | |
182 | if (verbose) | |
183 | fprintf(stderr, "Volume Trailer\n"); | |
184 | } else { | |
185 | fprintf(stderr, "Unrecognized Volume Header/Trailer\n"); | |
186 | } | |
187 | } | |
188 | ||
189 | int | |
190 | openOutFile(struct volumeHeader *headerPtr) | |
191 | { | |
192 | afs_int32 len; | |
193 | int ch; | |
194 | int rc; | |
195 | int oflag; | |
196 | int skip, first; | |
197 | afs_int64 size; | |
198 | ||
199 | /* If we were asked to skip this volume, then skip it */ | |
200 | if (nskip) { | |
201 | nskip--; | |
202 | return 0; | |
203 | } | |
204 | /* Skip if we are not to restore any */ | |
205 | if (!nrestore) | |
206 | return 0; | |
207 | ||
208 | /* Get the volume name and strip off the BK or RO extension */ | |
209 | if (outfile) { | |
210 | strcpy(filename, outfile); | |
211 | } else { | |
212 | strcpy(filename, headerPtr->volumeName); | |
213 | len = strlen(filename); | |
214 | if ((len > 7) && (strcmp(".backup", filename + len - 7) == 0)) { | |
215 | filename[len - 7] = 0; | |
216 | } else if ((len > 9) | |
217 | && (strcmp(".readonly", filename + len - 9) == 0)) { | |
218 | filename[len - 9] = 0; | |
219 | } | |
220 | } | |
221 | ||
222 | if (ask) { | |
223 | first = 1; | |
224 | skip = 0; | |
225 | printf("Press return to retrieve volume %s (%u) to file %s; " "s" | |
226 | " to skip\n", headerPtr->volumeName, headerPtr->volumeID, | |
227 | filename); | |
228 | do { | |
229 | ch = getchar(); | |
230 | if ((first == 1) && (ch == 's')) | |
231 | skip = 1; | |
232 | if ((first == 1) && (ch == 'q')) | |
233 | exit(0); | |
234 | first = 0; | |
235 | } while (ch != '\n'); | |
236 | if (skip) { | |
237 | printf("Will not restore volume %s\n", headerPtr->volumeName); | |
238 | return 0; | |
239 | } | |
240 | } else { | |
241 | printf("Retrieve volume %s (%u) to file %s\n", headerPtr->volumeName, | |
242 | headerPtr->volumeID, filename); | |
243 | } | |
244 | ||
245 | /* Should I append the date onto the end of the name? */ | |
246 | ||
247 | /* Open the file to write to */ | |
248 | if (headerPtr->contd == TC_VOLCONTD) { | |
249 | /* Continuation of dump */ | |
250 | oflag = USD_OPEN_RDWR; | |
251 | } else { | |
252 | /* An all new dump */ | |
253 | oflag = USD_OPEN_RDWR | USD_OPEN_CREATE; | |
254 | } | |
255 | rc = usd_Open(filename, oflag, 0664, &ofd); | |
256 | if (rc != 0) { | |
257 | fprintf(stderr, "Unable to open file %s. Skipping. Code = %d\n", | |
258 | filename, rc); | |
259 | nrestore--; | |
260 | return 0; | |
261 | } | |
262 | if (headerPtr->contd != TC_VOLCONTD) { | |
263 | size = 0; | |
264 | rc = USD_IOCTL(ofd, USD_IOCTL_SETSIZE, &size); | |
265 | if (rc != 0) { | |
266 | fprintf(stderr, "Unable to open file %s. Skipping. Code = %d\n", | |
267 | filename, rc); | |
268 | USD_CLOSE(ofd); | |
269 | nrestore--; | |
270 | return 0; | |
271 | } | |
272 | } | |
273 | ofdIsOpen = 1; | |
274 | return 0; | |
275 | } | |
276 | ||
277 | static void | |
278 | writeData(char *data, afs_int32 size) | |
279 | { | |
280 | int rc; | |
281 | u_int nwritten; | |
282 | ||
283 | if (!ofdIsOpen) | |
284 | return; | |
285 | rc = USD_WRITE(ofd, data, (u_int) size, &nwritten); | |
286 | if (rc != 0) { | |
287 | fprintf(stderr, "Unable to write volume data to file. Code = %d\n", | |
288 | rc); | |
289 | } | |
290 | return; | |
291 | } | |
292 | ||
293 | int | |
294 | writeLastBlocks(char *lastblock, char *lastblock2) | |
295 | { | |
296 | char trailer[12]; | |
297 | struct blockMark *bmark, *bmark2; | |
298 | char *data; | |
299 | char *data2 = NULL; | |
300 | int count, count2; | |
301 | int tlen, skip, pos; | |
302 | ||
303 | if (!ofdIsOpen) | |
304 | return 0; | |
305 | ||
306 | bmark = (struct blockMark *)lastblock; | |
307 | data = &lastblock[sizeof(struct blockMark)]; | |
308 | count = ntohl(bmark->count); | |
309 | if (lastblock2) { | |
310 | bmark2 = (struct blockMark *)lastblock2; | |
311 | data2 = &lastblock2[sizeof(struct blockMark)]; | |
312 | count2 = ntohl(bmark2->count); | |
313 | } else { | |
314 | count2 = 0; | |
315 | } | |
316 | ||
317 | /* | |
318 | * Strip off all but the last twelve bytes of the volume trailer | |
319 | */ | |
320 | skip = sizeof(struct volumeHeader) - 12; | |
321 | if (count >= skip) { | |
322 | count = count - skip; | |
323 | } else if (count + count2 >= skip) { | |
324 | count2 = count2 - (skip - count); | |
325 | count = 0; | |
326 | } else { | |
327 | fprintf(stderr, "Failed to strip off volume trailer (1).\n"); | |
328 | return 0; | |
329 | } | |
330 | ||
331 | /* volume trailer is somewhere in the last 12 bytes of the tape file. | |
332 | * The volume trailer may span tape blocks. */ | |
333 | if (count >= 12) { | |
334 | tlen = 0; | |
335 | memcpy(trailer, data + (count - 12), 12); | |
336 | } else { | |
337 | tlen = 12 - count; | |
338 | memcpy(trailer, data2 + (count2 - tlen), tlen); | |
339 | if (count != 0) { | |
340 | memcpy(trailer + tlen, data, count); | |
341 | } | |
342 | } | |
343 | ||
344 | for (pos = 0; pos <= 2; pos++) { | |
345 | if (strncmp(&trailer[pos], "H++NAME#", 8) == 0) { | |
346 | break; | |
347 | } | |
348 | if (tlen > 0) { | |
349 | tlen--; | |
350 | } | |
351 | } | |
352 | ||
353 | if (pos == 3) { | |
354 | fprintf(stderr, "Failed to strip off volume trailer (2).\n"); | |
355 | } else { | |
356 | if (count2 - tlen > 0) { | |
357 | writeData(data2, count2 - tlen); | |
358 | } | |
359 | if ((tlen == 0) && (count > 12 - pos)) { | |
360 | writeData(data, count - (12 - pos)); | |
361 | } | |
362 | } | |
363 | return 0; | |
364 | } | |
365 | ||
366 | int | |
367 | closeOutFile(void) | |
368 | { | |
369 | if (!ofdIsOpen) | |
370 | return 0; | |
371 | ||
372 | USD_CLOSE(ofd); | |
373 | ofdIsOpen = 0; | |
374 | ||
375 | /* Decrement the number of volumes to restore */ | |
376 | nrestore--; | |
377 | return 0; | |
378 | } | |
379 | ||
380 | static int | |
381 | WorkerBee(struct cmd_syndesc *as, void *arock) | |
382 | { | |
383 | char *tapedev; | |
384 | struct tapeLabel *label; | |
385 | struct fileMark *fmark; | |
386 | afs_int32 fmtype; | |
387 | struct blockMark *bmark; | |
388 | afs_int32 isheader, isdatablock; | |
389 | char *data; | |
390 | char *tblock; | |
391 | afs_int32 code; | |
392 | struct volumeHeader *volheaderPtr = NULL; | |
393 | int eod = 1; | |
394 | int rc; | |
395 | char *nextblock; /* We cycle through three tape blocks so we */ | |
396 | char *lastblock; /* can trim off the volume trailer from the */ | |
397 | char *lastblock2; /* end of each volume without having to back */ | |
398 | char *tapeblock1; /* up the output stream. */ | |
399 | char *tapeblock2; | |
400 | char *tapeblock3; | |
401 | ||
402 | tapedev = as->parms[0].items->data; /* -tape */ | |
403 | nrestore = | |
404 | (as->parms[1].items ? atol(as->parms[1].items->data) : 0x7fffffff); | |
405 | nskip = (as->parms[2].items ? atol(as->parms[2].items->data) : 0); | |
406 | if (as->parms[4].items) | |
407 | nskip = 0x7fffffff; /* -scan */ | |
408 | outfile = (as->parms[3].items ? as->parms[3].items->data : 0); | |
409 | ask = (as->parms[5].items ? 0 : 1); /* -noask */ | |
410 | printlabels = (as->parms[6].items ? 1 : 0); /* -label */ | |
411 | printheaders = (as->parms[7].items ? 1 : 0); /* -vheaders */ | |
412 | verbose = (as->parms[8].items ? 1 : 0); /* -verbose */ | |
413 | ||
414 | /* Open the tape device */ | |
415 | rc = usd_Open(tapedev, USD_OPEN_RDONLY, 0, &fd); | |
416 | if (rc != 0) { | |
417 | printf("Failed to open tape device %s. Code = %d\n", tapedev, rc); | |
418 | exit(1); | |
419 | } | |
420 | ||
421 | /* | |
422 | * Initialize the tape block buffers | |
423 | */ | |
424 | tapeblock1 = malloc(3 * 16384); | |
425 | if (tapeblock1 == NULL) { | |
426 | printf("Failed to allocate I/O buffers.\n"); | |
427 | exit(1); | |
428 | } | |
429 | tapeblock2 = tapeblock1 + 16384; | |
430 | tapeblock3 = tapeblock2 + 16384; | |
431 | ||
432 | nextblock = tapeblock1; | |
433 | lastblock = NULL; | |
434 | lastblock2 = NULL; | |
435 | ||
436 | /* Read each tape block deciding what to do with it */ | |
437 | do { /* while ((nskip!=0) && (nrestore!=0)) */ | |
438 | code = readblock(nextblock); | |
439 | if (code) { | |
440 | if (!eod) | |
441 | fprintf(stderr, "Tape device read error: %d\n", code); | |
442 | break; | |
443 | } | |
444 | isdatablock = 0; | |
445 | ||
446 | /* A data block can be either a volume header, volume trailer, | |
447 | * or actual data from a dump. | |
448 | */ | |
449 | bmark = (struct blockMark *)nextblock; | |
450 | label = (struct tapeLabel *)nextblock; | |
451 | fmark = (struct fileMark *)nextblock; | |
452 | if (ntohl(bmark->magic) == BLOCK_MAGIC) { | |
453 | if (verbose) | |
454 | printf("Data block\n"); | |
455 | isdatablock = 1; | |
456 | isheader = 0; | |
457 | data = &nextblock[sizeof(struct blockMark)]; | |
458 | if (strncmp(data, "H++NAME#", 8) == 0) { | |
459 | volheaderPtr = (struct volumeHeader *)data; | |
460 | printHeader(volheaderPtr, &isheader); | |
461 | } | |
462 | if (isheader) { | |
463 | code = openOutFile(volheaderPtr); | |
464 | nextblock = tapeblock1; | |
465 | lastblock = NULL; | |
466 | lastblock2 = NULL; | |
467 | } else { | |
468 | if (lastblock2 != NULL) { | |
469 | data = &lastblock2[sizeof(struct blockMark)]; | |
470 | bmark = (struct blockMark *)lastblock2; | |
471 | writeData(data, ntohl(bmark->count)); | |
472 | tblock = lastblock2; | |
473 | } else if (lastblock != NULL) { | |
474 | tblock = tapeblock2; | |
475 | } else { | |
476 | tblock = tapeblock3; | |
477 | } | |
478 | lastblock2 = lastblock; | |
479 | lastblock = nextblock; | |
480 | nextblock = tblock; | |
481 | } | |
482 | } | |
483 | ||
484 | /* Filemarks come in 3 forms: BEGIN, END, and EOD. | |
485 | * There is no information stored in filemarks. | |
486 | */ | |
487 | else if (ntohl(fmark->magic) == FILE_MAGIC) { | |
488 | fmtype = ntohl(fmark->nBytes); | |
489 | if (fmtype == FILE_BEGIN) { | |
490 | if (verbose) | |
491 | fprintf(stderr, "File mark volume begin\n"); | |
492 | } else if (fmtype == FILE_END) { | |
493 | if (verbose) | |
494 | fprintf(stderr, "File mark volume end\n"); | |
495 | } else if (fmtype == FILE_EOD) { | |
496 | if (verbose) | |
497 | fprintf(stderr, "File mark end-of-dump\n"); | |
498 | eod = 1; | |
499 | } | |
500 | } | |
501 | ||
502 | /* A dump label */ | |
503 | else if (ntohl(label->magic) == TAPE_MAGIC) { | |
504 | if (verbose) | |
505 | fprintf(stderr, "Dump label\n"); | |
506 | printLabel(label); | |
507 | eod = 0; | |
508 | } | |
509 | ||
510 | else { | |
511 | if (verbose) { | |
512 | fprintf(stderr, "Unrecognized tape block - end\n"); | |
513 | } | |
514 | } | |
515 | ||
516 | /* Anything other than a data block closes the out file. | |
517 | * At this point nextblock contains the end of tape file mark, | |
518 | * lastblock contains the last data block for the current volume, | |
519 | * and lastblock2 contains the second to last block for the current | |
520 | * volume. If the volume fits in a single block, lastblock2 will | |
521 | * be NULL. Call writeLastBlocks to strip off the dump trailer before | |
522 | * writing the last of the volume data to the dump file. The dump | |
523 | * trailer may span block boundaries. | |
524 | */ | |
525 | if (!isdatablock && lastblock) { | |
526 | writeLastBlocks(lastblock, lastblock2); | |
527 | closeOutFile(); | |
528 | nextblock = tapeblock1; | |
529 | lastblock = NULL; | |
530 | lastblock2 = NULL; | |
531 | } | |
532 | } while ((nskip != 0) || (nrestore != 0)); | |
533 | ||
534 | free(tapeblock1); | |
535 | ||
536 | return 0; | |
537 | } | |
538 | ||
539 | int | |
540 | main(int argc, char **argv) | |
541 | { | |
542 | struct cmd_syndesc *ts; | |
543 | ||
544 | setlinebuf(stdout); | |
545 | ||
546 | ts = cmd_CreateSyntax(NULL, WorkerBee, NULL, 0, | |
547 | "Restore volumes from backup tape"); | |
548 | cmd_AddParm(ts, "-tape", CMD_SINGLE, CMD_REQUIRED, "tape device"); | |
549 | cmd_AddParm(ts, "-restore", CMD_SINGLE, CMD_OPTIONAL, | |
550 | "# volumes to restore"); | |
551 | cmd_AddParm(ts, "-skip", CMD_SINGLE, CMD_OPTIONAL, "# volumes to skip"); | |
552 | cmd_AddParm(ts, "-file", CMD_SINGLE, CMD_OPTIONAL, "filename"); | |
553 | cmd_AddParm(ts, "-scan", CMD_FLAG, CMD_OPTIONAL, "Scan the tape"); | |
554 | cmd_AddParm(ts, "-noask", CMD_FLAG, CMD_OPTIONAL, | |
555 | "Prompt for each volume"); | |
556 | cmd_AddParm(ts, "-label", CMD_FLAG, CMD_OPTIONAL, "Display dump label"); | |
557 | cmd_AddParm(ts, "-vheaders", CMD_FLAG, CMD_OPTIONAL, | |
558 | "Display volume headers"); | |
559 | cmd_AddParm(ts, "-verbose", CMD_FLAG, CMD_OPTIONAL, "verbose"); | |
560 | ||
561 | return cmd_Dispatch(argc, argv); | |
562 | } |