Version 2.0.0: update sources
[jvcard.git] / src / be / nikiroo / jvcard / launcher / Main.java
CommitLineData
7da41ecd
NR
1package be.nikiroo.jvcard.launcher;
2
3import java.io.File;
f06c8100 4import java.io.FileInputStream;
7da41ecd 5import java.io.IOException;
f06c8100 6import java.io.InputStream;
7da41ecd 7import java.lang.reflect.Field;
7da41ecd
NR
8import java.net.Socket;
9import java.nio.charset.Charset;
10import java.util.LinkedList;
11import java.util.List;
12
26d254a3
NR
13import javax.imageio.ImageIO;
14
7da41ecd 15import be.nikiroo.jvcard.Card;
26d254a3
NR
16import be.nikiroo.jvcard.Contact;
17import be.nikiroo.jvcard.Data;
18import be.nikiroo.jvcard.TypeInfo;
5ad0e17e 19import be.nikiroo.jvcard.launcher.CardResult.MergeCallback;
e643219c 20import be.nikiroo.jvcard.launcher.Optional.NotSupportedException;
7da41ecd 21import be.nikiroo.jvcard.parsers.Format;
845fb1d7 22import be.nikiroo.jvcard.remote.Command;
7da41ecd 23import be.nikiroo.jvcard.remote.SimpleSocket;
f06c8100
NR
24import be.nikiroo.jvcard.resources.DisplayBundle;
25import be.nikiroo.jvcard.resources.DisplayOption;
26import be.nikiroo.jvcard.resources.RemoteBundle;
27import be.nikiroo.jvcard.resources.StringId;
28import be.nikiroo.jvcard.resources.TransBundle;
29import be.nikiroo.utils.ImageUtils;
30import be.nikiroo.utils.StringUtils;
31import be.nikiroo.utils.Version;
32import be.nikiroo.utils.resources.Bundles;
7da41ecd
NR
33
34/**
35 * This class contains the runnable Main method. It will parse the user supplied
36 * parameters and take action based upon those. Most of the time, it will start
37 * a MainWindow.
38 *
39 * @author niki
f06c8100 40 *
7da41ecd
NR
41 */
42public class Main {
43 static public final String APPLICATION_TITLE = "jVcard";
7da41ecd
NR
44
45 static private final int ERR_NO_FILE = 1;
46 static private final int ERR_SYNTAX = 2;
47 static private final int ERR_INTERNAL = 3;
e119a1c1 48 static private TransBundle transService;
7da41ecd 49
30a4aa17
NR
50 static private String defaultFn;
51 static private boolean forceComputedFn;
52
26d254a3 53 enum Mode {
88eb8122 54 CONTACT_MANAGER, I18N, SERVER, LOAD_PHOTO, SAVE_PHOTO, SAVE_CONFIG, HELP
26d254a3
NR
55 }
56
7da41ecd 57 /**
9b8cb729 58 * Translate the given {@link StringId} into user text.
7da41ecd 59 *
9b8cb729 60 * @param stringId
7da41ecd 61 * the ID to translate
9b8cb729
NR
62 * @param values
63 * the values to insert instead of the place holders in the
64 * translation
7da41ecd 65 *
9b8cb729 66 * @return the translated text with the given value where required
7da41ecd 67 */
e119a1c1 68 static public String trans(StringId id, Object... values) {
59597d59 69 return transService.getString(id, values);
7da41ecd
NR
70 }
71
72 /**
73 * Check if unicode characters should be used.
74 *
75 * @return TRUE to allow unicode
76 */
77 static public boolean isUnicode() {
78 return transService.isUnicode();
79 }
80
81 /**
82 * Start the application.
83 *
84 * <p>
85 * The returned exit codes are:
86 * <ul>
87 * <li>1: no files to open</li>
88 * <li>2: invalid syntax</li>
89 * <li>3: internal error</li>
90 * </ul>
91 * </p>
92 *
93 * @param args
94 * the parameters (see <tt>--help</tt> to know which are
95 * supported)
96 */
97 public static void main(String[] args) {
98 Boolean textMode = null;
99 boolean noMoreParams = false;
100 boolean filesTried = false;
101
102 // get the "system default" language to help translate the --help
103 // message if needed
104 String language = null;
e119a1c1 105 transService = new TransBundle(language);
7da41ecd
NR
106
107 boolean unicode = transService.isUnicode();
26d254a3 108 String dir = null;
7da41ecd 109 List<String> files = new LinkedList<String>();
26d254a3
NR
110 int port = -1;
111 Mode mode = Mode.CONTACT_MANAGER;
112 String format = null;
7da41ecd
NR
113 for (int index = 0; index < args.length; index++) {
114 String arg = args[index];
115 if (!noMoreParams && arg.equals("--")) {
116 noMoreParams = true;
117 } else if (!noMoreParams && arg.equals("--help")) {
88eb8122
NR
118 if (mode != Mode.CONTACT_MANAGER) {
119 SERR(StringId.CLI_SERR_MODES);
120 return;
121 }
122 mode = Mode.HELP;
7da41ecd
NR
123 } else if (!noMoreParams && arg.equals("--tui")) {
124 textMode = true;
125 } else if (!noMoreParams && arg.equals("--gui")) {
126 textMode = false;
127 } else if (!noMoreParams && arg.equals("--noutf")) {
128 unicode = false;
129 transService.setUnicode(unicode);
130 } else if (!noMoreParams && arg.equals("--lang")) {
131 index++;
132 if (index >= args.length) {
88eb8122 133 SERR(StringId.CLI_SERR_NOLANG);
7da41ecd
NR
134 return;
135 }
136
137 language = args[index];
e119a1c1 138 transService = new TransBundle(language);
7da41ecd
NR
139 transService.setUnicode(unicode);
140 } else if (!noMoreParams && arg.equals("--config")) {
141 index++;
142 if (index >= args.length) {
88eb8122 143 SERR(StringId.CLI_SERR_NODIR);
7da41ecd
NR
144 return;
145 }
146
147 Bundles.setDirectory(args[index]);
e119a1c1 148 transService = new TransBundle(language);
7da41ecd 149 transService.setUnicode(unicode);
e119a1c1
NR
150 } else if (!noMoreParams && arg.equals("--save-config")) {
151 index++;
152 if (index >= args.length) {
88eb8122 153 SERR(StringId.CLI_SERR_NODIR);
e119a1c1
NR
154 return;
155 }
156 dir = args[index];
157
158 if (mode != Mode.CONTACT_MANAGER) {
88eb8122 159 SERR(StringId.CLI_SERR_MODES);
e119a1c1
NR
160 return;
161 }
162 mode = Mode.SAVE_CONFIG;
7da41ecd 163 } else if (!noMoreParams && arg.equals("--server")) {
26d254a3 164 if (mode != Mode.CONTACT_MANAGER) {
88eb8122 165 SERR(StringId.CLI_SERR_MODES);
26d254a3
NR
166 return;
167 }
168 mode = Mode.SERVER;
169
7da41ecd
NR
170 index++;
171 if (index >= args.length) {
88eb8122 172 SERR(StringId.CLI_SERR_NOPORT);
7da41ecd
NR
173 return;
174 }
175
176 try {
177 port = Integer.parseInt(args[index]);
178 } catch (NumberFormatException e) {
88eb8122 179 SERR(StringId.CLI_SERR_BADPORT, "" + args[index]);
7da41ecd
NR
180 return;
181 }
182 } else if (!noMoreParams && arg.equals("--i18n")) {
26d254a3 183 if (mode != Mode.CONTACT_MANAGER) {
88eb8122 184 SERR(StringId.CLI_SERR_MODES);
26d254a3
NR
185 return;
186 }
187 mode = Mode.I18N;
188
7da41ecd
NR
189 index++;
190 if (index >= args.length) {
88eb8122 191 SERR(StringId.CLI_SERR_NODIR);
7da41ecd
NR
192 return;
193 }
9b8cb729 194
26d254a3
NR
195 dir = args[index];
196 } else if (!noMoreParams
197 && (arg.equals("--load-photo")
198 || arg.equals("--save-photo") || arg
199 .equals("--only-photo"))) {
200 if (mode != Mode.CONTACT_MANAGER) {
88eb8122 201 SERR(StringId.CLI_SERR_MODES);
26d254a3
NR
202 return;
203 }
204
205 if (arg.equals("--load-photo")) {
206 mode = Mode.LOAD_PHOTO;
207 } else if (arg.equals("--save-photo")) {
208 mode = Mode.SAVE_PHOTO;
26d254a3
NR
209 }
210
211 index++;
212 if (index >= args.length) {
88eb8122 213 SERR(StringId.CLI_SERR_NODIR);
26d254a3
NR
214 return;
215 }
216
217 dir = args[index];
218
219 index++;
220 if (index >= args.length) {
88eb8122 221 SERR(StringId.CLI_SERR_NOFORMAT);
26d254a3
NR
222 return;
223 }
224
225 format = args[index];
7da41ecd
NR
226 } else {
227 filesTried = true;
228 files.addAll(open(arg));
229 }
230 }
9b8cb729 231
f578f3af 232 // Force headless mode if we run in forced-text mode
26d254a3 233 if (mode != Mode.CONTACT_MANAGER || (textMode != null && textMode)) {
f578f3af
NR
234 // same as -Djava.awt.headless=true
235 System.setProperty("java.awt.headless", "true");
236 }
7da41ecd
NR
237
238 if (unicode) {
239 utf8();
240 }
241
30a4aa17
NR
242 // N/FN fix information:
243 readNFN();
244
7da41ecd 245 // Error management:
26d254a3 246 if (mode == Mode.SERVER && files.size() > 0) {
88eb8122
NR
247 SERR(StringId.CLI_SERR_NOLANG, "--server");
248 return;
26d254a3 249 } else if (mode == Mode.I18N && files.size() > 0) {
88eb8122
NR
250 SERR(StringId.CLI_SERR_NOLANG, "--i18n");
251 return;
26d254a3 252 } else if (mode == Mode.I18N && language == null) {
88eb8122 253 SERR(StringId.CLI_SERR_NOLANG);
26d254a3
NR
254 } else if ((mode == Mode.CONTACT_MANAGER || mode == Mode.SAVE_PHOTO || mode == Mode.LOAD_PHOTO)
255 && files.size() == 0) {
7da41ecd
NR
256 if (files.size() == 0 && !filesTried) {
257 files.addAll(open("."));
258 }
259
260 if (files.size() == 0) {
88eb8122 261 ERR(StringId.CLI_ERR, StringId.CLI_ERR_NOFILES, ERR_NO_FILE);
7da41ecd
NR
262 return;
263 }
264 }
265 //
266
26d254a3 267 switch (mode) {
e119a1c1
NR
268 case SAVE_CONFIG: {
269 try {
270 if (!new File(dir).isDirectory()) {
271 if (!new File(dir).mkdir()) {
88eb8122
NR
272 System.err.println(trans(
273 StringId.CLI_ERR_CANNOT_CREATE_CONFDIR, dir));
e119a1c1
NR
274 }
275 }
276
e27d1404 277 new TransBundle().updateFile(dir); // default locale
f06c8100 278 for (String lang : new TransBundle().getKnownLanguages()) {
e27d1404
NR
279 new TransBundle(lang).updateFile(dir);
280 }
281
f06c8100 282 // new UIColors().updateFile(dir);
e119a1c1
NR
283 new DisplayBundle().updateFile(dir);
284 new RemoteBundle().updateFile(dir);
285 } catch (IOException e) {
286 e.printStackTrace();
59597d59 287 System.err.flush();
e119a1c1
NR
288 System.exit(ERR_INTERNAL);
289 }
290 break;
291 }
26d254a3 292 case SERVER: {
7da41ecd 293 try {
02b341aa 294 Optional.runServer(port);
e643219c
NR
295 } catch (IOException e) {
296 ERR(StringId.CLI_ERR, StringId.CLI_ERR_CANNOT_START,
297 ERR_INTERNAL);
298 return;
299 } catch (NotSupportedException e) {
300 if (!e.isCompiledIn()) {
88eb8122
NR
301 ERR(StringId.CLI_ERR, StringId.CLI_ERR_NO_REMOTING,
302 ERR_INTERNAL);
303 return;
d459d7e1
NR
304 } else {
305 e.printStackTrace();
306 ERR(StringId.CLI_ERR, StringId.CLI_ERR, ERR_INTERNAL);
307 return;
7da41ecd
NR
308 }
309 }
26d254a3
NR
310 break;
311 }
312 case I18N: {
7da41ecd 313 try {
e119a1c1 314 transService.updateFile(dir);
7da41ecd 315 } catch (IOException e) {
d459d7e1
NR
316 ERR(StringId.CLI_ERR, StringId.CLI_ERR_CANNOT_CREATE_LANG,
317 ERR_INTERNAL);
318 return;
7da41ecd 319 }
26d254a3
NR
320 break;
321 }
26d254a3
NR
322 case LOAD_PHOTO: {
323 for (String file : files) {
324 try {
325 Card card = getCard(file, null).getCard();
326 for (Contact contact : card) {
327 String filename = contact.toString(format, "");
f29274a7 328 File f = new File(dir, filename);
26d254a3
NR
329
330 if (f.exists()) {
e3fe9834 331 System.out.println("Loading " + f);
26d254a3 332 try {
a1783d00
NR
333 String type = "jpeg";
334 int dotIndex = filename.indexOf('.');
335 if (dotIndex >= 0
336 && (dotIndex + 1) < filename.length()) {
337 type = filename.substring(dotIndex + 1)
338 .toLowerCase();
339 }
340
f06c8100
NR
341 String b64;
342 InputStream in = null;
343 try {
344 in = new FileInputStream(f);
345 b64 = ImageUtils.toBase64(in);
346 } finally {
347 if (in != null) {
348 in.close();
349 }
350 }
26d254a3 351
88eb8122
NR
352 // remove previous photos:
353 for (Data photo = contact
354 .getPreferredData("PHOTO"); photo != null; photo = contact
355 .getPreferredData("PHOTO")) {
356 photo.delete();
26d254a3 357 }
88eb8122 358 //
26d254a3
NR
359
360 List<TypeInfo> types = new LinkedList<TypeInfo>();
361 types.add(new TypeInfo("ENCODING", "b"));
a1783d00 362 types.add(new TypeInfo("TYPE", type));
26d254a3
NR
363 Data photo = new Data(types, "PHOTO", b64, null);
364 contact.add(photo);
365 } catch (IOException e) {
366 System.err.println("Cannot read photo: "
367 + filename);
368 }
369 }
370 }
371 card.save();
372 } catch (IOException e) {
88eb8122
NR
373 System.err
374 .println(trans(StringId.CLI_ERR_CANNOT_OPEN, file));
26d254a3
NR
375 }
376 }
377 break;
378 }
379 case SAVE_PHOTO: {
380 for (String file : files) {
381 try {
382 Card card = getCard(file, null).getCard();
383 for (Contact contact : card) {
384 Data photo = contact.getPreferredData("PHOTO");
385 if (photo != null) {
386 String filename = contact.toString(format, "");
387 File f = new File(dir, filename + ".png");
e3fe9834 388 System.out.println("Saving " + f);
26d254a3
NR
389 try {
390 ImageIO.write(
f06c8100 391 ImageUtils.fromBase64(photo.getValue()),
26d254a3
NR
392 "png", f);
393 } catch (IOException e) {
88eb8122
NR
394 System.err.println(trans(
395 StringId.CLI_ERR_CANNOT_SAVE_PHOTO,
396 contact.getPreferredDataValue("FN")));
26d254a3
NR
397 }
398 }
399 }
400 } catch (IOException e) {
88eb8122
NR
401 System.err
402 .println(trans(StringId.CLI_ERR_CANNOT_OPEN, file));
26d254a3
NR
403 }
404 }
405 break;
406 }
407 case CONTACT_MANAGER: {
7da41ecd 408 try {
02b341aa 409 Optional.startTui(textMode, files);
e643219c
NR
410 } catch (IOException e) {
411 ERR(StringId.CLI_ERR, StringId.CLI_ERR_CANNOT_START,
412 ERR_NO_FILE);
413 return;
414 } catch (NotSupportedException e) {
415 if (!e.isCompiledIn()) {
88eb8122
NR
416 ERR(StringId.CLI_ERR, StringId.CLI_ERR_NO_TUI, ERR_INTERNAL);
417 return;
d459d7e1
NR
418 } else {
419 e.printStackTrace();
420 ERR(StringId.CLI_ERR, StringId.CLI_ERR, ERR_INTERNAL);
421 return;
7da41ecd
NR
422 }
423 }
26d254a3
NR
424 break;
425 }
88eb8122 426 case HELP: {
f06c8100
NR
427 System.out.println(APPLICATION_TITLE + " "
428 + Version.getCurrentVersion());
88eb8122
NR
429 System.out.println();
430
431 System.out.println(trans(StringId.CLI_HELP));
432 System.out.println();
433
434 System.out.println(trans(StringId.CLI_HELP_MODES));
435 System.out.println("\t--help : "
436 + trans(StringId.CLI_HELP_MODE_HELP));
437 System.out.println("\t(--tui|--gui) (--noutf) ... : "
438 + trans(StringId.CLI_HELP_MODE_CONTACT_MANAGER));
439 System.out.println("\t--server PORT ... : "
440 + trans(StringId.CLI_HELP_MODE_SERVER));
441 System.out.println("\t--save-config DIR : "
442 + trans(StringId.CLI_HELP_MODE_SAVE_CONFIG));
443 System.out.println("\t--i18n DIR ---lang LANG : "
444 + trans(StringId.CLI_HELP_MODE_I18N));
445 System.out.println("\t--load-photo DIR FORMAT ... : "
446 + trans(StringId.CLI_HELP_MODE_LOAD_PHOTO));
447 System.out.println("\t--save-photo DIR FORMAT ... : "
448 + trans(StringId.CLI_HELP_MODE_SAVE_PHOTO));
449 System.out.println();
450
451 System.out.println(trans(StringId.CLI_HELP_OPTIONS));
452 System.out.println("\t-- : " + trans(StringId.CLI_HELP_DD));
453 System.out.println("\t--lang LANG : "
454 + trans(StringId.CLI_HELP_LANG));
455 System.out.println("\t--tui : " + trans(StringId.CLI_HELP_TUI));
456 System.out.println("\t--gui : " + trans(StringId.CLI_HELP_GUI));
99f631de
NR
457 System.out.println("\t--noutf : "
458 + trans(StringId.CLI_HELP_NOUTF_OPTION));
88eb8122
NR
459 System.out.println("\t--config : "
460 + trans(StringId.CLI_HELP_CONFIG));
461 System.out.println();
462
463 System.out.println(trans(StringId.CLI_HELP_FOOTER));
464 System.out.println();
465
466 }
7da41ecd
NR
467 }
468 }
469
470 /**
471 * Return the {@link Card} corresponding to the given resource name -- a
30a4aa17
NR
472 * file or a remote jvcard URL.
473 *
474 * <p>
475 * Will also fix the FN if required (see display.properties).
476 * </p>
7da41ecd
NR
477 *
478 * @param input
479 * a filename or a remote jvcard url with named resource (e.g.:
480 * <tt>jvcard://localhost:4444/coworkers.vcf</tt>)
5ad0e17e
NR
481 * @param callback
482 * the {@link MergeCallback} to call in case of conflict, or NULL
483 * to disallow conflict management (the {@link Card} will not be
484 * allowed to synchronise in case of conflicts)
7da41ecd
NR
485 *
486 * @return the {@link Card}
487 *
488 * @throws IOException
489 * in case of IO error or remoting not available
490 */
5ad0e17e
NR
491 static public CardResult getCard(String input, MergeCallback callback)
492 throws IOException {
7da41ecd
NR
493 boolean remote = false;
494 Format format = Format.Abook;
495 String ext = input;
496 if (ext.contains(".")) {
497 String tab[] = ext.split("\\.");
498 if (tab.length > 1 && tab[tab.length - 1].equalsIgnoreCase("vcf")) {
499 format = Format.VCard21;
500 }
501 }
502
503 if (input.contains("://")) {
504 format = Format.VCard21;
505 remote = true;
506 }
507
5ad0e17e 508 CardResult card = null;
7da41ecd
NR
509 try {
510 if (remote) {
5ad0e17e 511 card = Optional.syncCard(input, callback);
7da41ecd 512 } else {
5ad0e17e
NR
513 card = new CardResult(new Card(new File(input), format), false,
514 false, false);
7da41ecd
NR
515 }
516 } catch (IOException ioe) {
517 throw ioe;
e643219c 518 } catch (NotSupportedException e) {
4298276a 519 throw new IOException("Remoting support not available", e);
7da41ecd
NR
520 }
521
30a4aa17
NR
522 // Fix the FN value
523 if (defaultFn != null) {
524 try {
525 for (Contact contact : card.getCard()) {
526 Data name = contact.getPreferredData("FN");
527 if (name == null || name.getValue().length() == 0
528 || forceComputedFn) {
99f631de 529 name.setValue(contact.toString(defaultFn, "").trim());
30a4aa17
NR
530 }
531 }
532 } catch (Exception e) {
533 // sync failed -> getCard() throws.
534 // do not update.
535 }
536 }
537
7da41ecd
NR
538 return card;
539 }
540
7da41ecd
NR
541 /**
542 * Open the given path and add all its files if it is a directory or just
543 * this one if not to the returned list.
544 *
545 * @param path
546 * the path to open
547 *
548 * @return the list of opened files
549 */
550 static private List<String> open(String path) {
551 List<String> files = new LinkedList<String>();
552
553 if (path != null && path.startsWith("jvcard://")) {
554 if (path.endsWith("/")) {
555 files.addAll(list(path));
556 } else {
557 files.add(path);
558 }
559 } else {
560 File file = new File(path);
561 if (file.exists()) {
562 if (file.isDirectory()) {
563 for (File subfile : file.listFiles()) {
564 if (!subfile.isDirectory())
565 files.add(subfile.getAbsolutePath());
566 }
567 } else {
568 files.add(file.getAbsolutePath());
569 }
570 } else {
571 System.err.println("File or directory not found: \"" + path
572 + "\"");
573 }
574 }
575
576 return files;
577 }
578
579 /**
580 * List all the available {@link Card}s on the given network location (which
581 * is expected to be a jVCard remote server, obviously).
582 *
583 * @param path
584 * the jVCard remote server path (e.g.:
585 * <tt>jvcard://localhost:4444/</tt>)
586 *
587 * @return the list of {@link Card}s
588 */
589 static private List<String> list(String path) {
590 List<String> files = new LinkedList<String>();
591
592 try {
593 String host = path.split("\\:")[1].substring(2);
594 int port = Integer.parseInt(path.split("\\:")[2].replaceAll("/$",
595 ""));
596 SimpleSocket s = new SimpleSocket(new Socket(host, port),
597 "sync client");
598 s.open(true);
599
845fb1d7 600 s.sendCommand(Command.LIST_CARD);
7da41ecd
NR
601 for (String p : s.receiveBlock()) {
602 files.add(path
603 + p.substring(StringUtils.fromTime(0).length() + 1));
604 }
605 s.close();
606 } catch (Exception e) {
607 e.printStackTrace();
608 }
609
610 return files;
611 }
612
613 /**
614 * Really, really ask for UTF-8 encoding.
615 */
616 static private void utf8() {
617 try {
618 System.setProperty("file.encoding", "UTF-8");
619 Field charset = Charset.class.getDeclaredField("defaultCharset");
620 charset.setAccessible(true);
621 charset.set(null, null);
622 } catch (SecurityException e) {
623 } catch (NoSuchFieldException e) {
624 } catch (IllegalArgumentException e) {
625 } catch (IllegalAccessException e) {
626 }
627 }
30a4aa17
NR
628
629 /**
630 * Read display.properties to know if we should fix the FN field when empty,
631 * or always, or never.
632 */
633 static private void readNFN() {
e119a1c1 634 DisplayBundle map = new DisplayBundle();
30a4aa17 635
e119a1c1
NR
636 defaultFn = map.getString(DisplayOption.CONTACT_DETAILS_DEFAULT_FN);
637
638 forceComputedFn = map.getBoolean(
639 DisplayOption.CONTACT_DETAILS_SHOW_COMPUTED_FN, false);
30a4aa17 640 }
88eb8122
NR
641
642 /**
643 * Syntax error detected, closing the application with an error message.
644 *
645 * @param err
646 * the syntax error case
647 */
648 static private void SERR(StringId err, Object... values) {
649 ERR(StringId.CLI_SERR, err, ERR_SYNTAX, values);
650 }
651
652 /**
653 * Error detected, closing the application with an error message.
654 *
655 * @param err
656 * the error case
657 * @param suberr
658 * the suberror or NULL if none
659 * @param CODE
660 * the error code as declared above
661 */
662 static private void ERR(StringId err, StringId suberr, int CODE,
663 Object... subvalues) {
664 if (suberr == null)
665 System.err.println(trans(err));
666 else
667 System.err.println(trans(err, trans(suberr, subvalues)));
668
59597d59 669 System.err.flush();
88eb8122
NR
670 System.exit(CODE);
671 }
7da41ecd 672}