source: trunk/FACT++/src/root2csv.cc@ 19806

Last change on this file since 19806 was 19806, checked in by tbretz, 5 years ago
Some improvements to first/max warning:
File size: 24.2 KB
Line 
1#include <boost/regex.hpp>
2#include <boost/filesystem.hpp>
3#include <boost/algorithm/string/join.hpp>
4
5#include "tools.h"
6#include "Time.h"
7#include "Splitting.h"
8
9#include <TROOT.h>
10#include <TSystem.h>
11#include <TChain.h>
12#include <TLeaf.h>
13#include <TError.h>
14#include <TTreeFormula.h>
15#include <TTreeFormulaManager.h>
16
17#include "FileEntry.h"
18
19using namespace std;
20namespace fs = boost::filesystem;
21
22// ------------------------------------------------------------------------
23
24void SetupConfiguration(Configuration &conf)
25{
26 po::options_description control("Root to SQL");
27 control.add_options()
28 ("file", vars<string>()->required(),"The root files to read from")
29 ("out,o", var<string>()->required(), "Output file name")
30 ("force,f", po_switch(), "Force overwrite if output file already exists.")
31 ("append,a", po_switch(), "Append to an existing file (not check for the format is done!)")
32 ("tree,t", var<string>("Events"), "Name of the root tree to convert")
33 ("ignore", vars<string>(), "Ignore the given leaf, if the given regular expression matches")
34 ("alias.*", var<string>(), "Define an alias")
35 ("auto-alias", vars<Configuration::Map>(),"Regular expression to define aliases from the branch names automatically")
36 ("header", var<uint16_t>(uint16_t(0)),"Type of header line (0: preceeding #, 1: without preceeding #, 2: none)")
37 ("add.*", var<string>(), "Define an additional column")
38 ("selector,s", var<string>("1"), "Define a selector for the columns (colums where this evaluates to a value <=0 are discarded)")
39 ("skip", po_switch(), "Discards all default leaves and writes only the columns defined by --add.*")
40 ("first", var<int64_t>(int64_t(0)), "First event to start with (default: 0), mainly for test purpose")
41 ("max", var<int64_t>(int64_t(0)), "Maximum number of events to process (0: all), mainly for test purpose")
42 //("const.*", var<string>(), "Insert a constant number into the given column (--const.mycolumn=5). A special case is `/.../.../`")
43 ("dry-run", po_switch(), "Do not create or manipulate any output file")
44 ;
45
46 po::options_description debug("Debug options");
47 debug.add_options()
48 ("print-ls", po_switch(), "Calls TFile::ls()")
49 ("print-branches", po_switch(), "Print the branches found in the tree")
50 ("print-leaves", po_switch(), "Print the leaves found in the tree (this is what is processed)")
51 ("verbose,v", var<uint16_t>(1), "Verbosity (0: quiet, 1: default, 2: more, 3, ...)")
52 ;
53
54 po::positional_options_description p;
55 p.add("file", -1); // All positional options
56
57 conf.AddOptions(control);
58 conf.AddOptions(Tools::Splitting::options());
59 conf.AddOptions(debug);
60 conf.SetArgumentPositions(p);
61}
62
63void PrintUsage()
64{
65 cout <<
66 "root2csv - Reads data from a root tree and writes a csv file\n"
67 "\n"
68 "For convenience, this documentation uses the extended version of the options, "
69 "refer to the output below to get the abbreviations.\n"
70 "\n"
71 "Similar functionaliy is also provided by root2sql. In addition to root2sql, "
72 "this tool is more flexible in the slection of columns and adds the possibility "
73 "to use formulas (implemented through TTreeFormula) to calculate values for "
74 "additional columns. Note that root can even write complex data like a TH1F "
75 "into a file. Here, only numeric columns are supported.\n"
76 "\n"
77 "Input files are given as positional arguments or with --file. "
78 "As files are read by adding them through TChain::Add, wildcards are "
79 "supported in file names. Note that on the command lines, file names "
80 "with wildcards have to be escaped in quotation marks if the wildcards "
81 "should be evaluated by the program and not by the shell. The output base "
82 "name of the output file(s) is given with --out.\n"
83 "\n"
84 "The format of the first line on the file is defined with the --header option:\n"
85 " 0: '# Col1 Col2 Col3 ...'\n"
86 " 1: 'Col1 Col2 Col3 ...'\n"
87 " 2: first data row\n"
88 "\n"
89 "As default, existing files are not overwritten. To force overwriting use "
90 "--force. To append data to existing files use --append. Note that no "
91 "check is done if this created valid and reasonable files.\n"
92 "\n"
93 "Each root tree has branches and leaves (the basic data types). These leaves can "
94 "be read independently of the classes which were used to write the root file. "
95 "The default tree to read from is 'Events' but the name can be overwritten "
96 "using --tree. The default table name to fill the data into is identical to "
97 "the tree name. It can be overwritten using --table.\n"
98 "\n"
99 "To get a list of the contents (keys and trees) of a root file, you can use --print-ls. "
100 "The name of each column to which data is filled from a leave is obtained from "
101 "the leaves' names. The leave names can be checked using --print-leaves. "
102 "A --print-branches exists for convenience to print only the high-level branches.\n"
103 "\n"
104 "Assuming a leaf with name MHillas.fWidth and a leaf with MHillas.fLength, "
105 "a new column can be added with name Area by\n"
106 " --add.Area='TMath::TwoPi()*MHillas.fWidth*MHillas.fLength'\n"
107 "\n"
108 "To simplify expression, root allows to define aliases, for example\n"
109 " --alias.Width='MHillas.fWidth'\n"
110 " --alias.Length='MHillas.fLength'\n"
111 "\n"
112 "This can then be used to simplyfy the above expression as\n"
113 " --add.Area='TMath::TwoPi()*Width*Length'\n"
114 "\n"
115 "Sometimes leaf names might be quite unconvenient like MTime.fTime.fMilliSec or "
116 "just MHillas.fWidth. To allow to simplify column names, regular expressions "
117 "(using boost's regex) can be defined to change the names. Note that these regular "
118 "expressions are applied one by one on each leaf's name. A valid expression could "
119 "be:\n"
120 " --auto-alias=MHillas\\.f/\n"
121 "which would remove all occurances of 'MHillas.f'. This option can be used more than "
122 "once. They are applied in sequence. A single match does not stop the sequence. "
123 "In addition to replacing the column names accordingly, a alias is created "
124 "automatically allowing to access the columns in a formula with the new name.\n"
125 "\n"
126 "Sometimes it might also be convenient to skip a leaf, i.e. not writing the "
127 "coresponding column in the output file. This can be done with "
128 "the --ignore resource. If the given regular expresion yields a match, the "
129 "leaf will be ignored. An automatic alias would still be created and the "
130 "leaf could still be used in a formula. Example\n"
131 " --ignore=ThetaSq\\..*\n"
132 "will skip all leaved which start with 'ThetaSq.'. This directive can be given "
133 "more than once. The so defined ignore list is applied entry-wise, first to the "
134 "raw leaf names, then to the aliased names.\n"
135 "\n"
136 "To select only certain extries from the file, a selector (cut) can be defined "
137 "in the same style as the --add directives, for exmple:\n"
138 " --selector='MHillas.fLength*Width<0'\n"
139 "Note that the selctor is not evaluated to a boolean expression (==0 or !=0) "
140 "but all positive none zero values are considered 'true' (select the entry) "
141 "and all negative values are considered 'fales' (discard the entry).\n"
142 "\n"
143 << Tools::Splitting::usage() <<
144 "\n"
145 "In case of success, 0 is returned, a value>0 otherwise.\n"
146 "\n"
147 "Usage: root2csv input1.root [input2.root ...] -o output.csv [-t tree] [-u] [-f] [-n] [-vN] [-cN]\n"
148 "\n"
149 ;
150 cout << endl;
151}
152
153void ErrorHandlerAll(Int_t level, Bool_t abort, const char *location, const char *msg)
154{
155 if (string(msg).substr(0,24)=="no dictionary for class ")
156 return;
157 if (string(msg).substr(0,15)=="unknown branch ")
158 return;
159
160 DefaultErrorHandler(level, abort, location, msg);
161}
162
163// --------------------------- Write Header --------------------------------
164void WriteHeader(ostream &out, const vector<FileEntry::Container> &vec, const vector<TTreeFormula*> &form, bool skip, uint16_t header)
165{
166 if (header>1)
167 return;
168 if (header==0)
169 out << "# ";
170
171 vector<string> join;
172
173 if (!skip)
174 {
175 for (auto v=vec.cbegin(); v!=vec.cend(); v++)
176 {
177 const size_t N = v->num;
178 for (size_t i=0; i<N; i++)
179 {
180 string name = v->column;
181 if (N!=1)
182 name += "["+to_string(i)+"]";
183 join.emplace_back(name);
184 }
185 }
186 }
187
188 for (auto v=form.cbegin(); v!=form.cend(); v++)
189 join.emplace_back((*v)->GetName());
190
191 out << boost::join(join, " ") << "\n";
192}
193
194int CheckFile(TString &path, bool force, int verbose)
195{
196 gSystem->ExpandPathName(path);
197
198 FileStat_t stat;
199 const Int_t exist = !gSystem->GetPathInfo(path, stat);
200 const Bool_t _write = !gSystem->AccessPathName(path, kWritePermission) && R_ISREG(stat.fMode);
201
202 if (exist)
203 {
204 if (!_write)
205 {
206 cerr << "File '" << path << "' is not writable." << endl;
207 return 2;
208 }
209
210 if (!force)
211 {
212 cerr << "File '" << path << "' already exists." << endl;
213 return 3;
214 }
215 else
216 {
217 if (verbose>0)
218 cerr << "File '" << path << "' will be overwritten." << endl;
219 }
220 }
221 return exist ? 0 : -1;
222}
223
224void GetLeaves(vector<string> &list, const TTreeFormula &f)
225{
226 int i=0;
227 while (1)
228 {
229 const auto l = f.GetLeaf(i++);
230 if (!l)
231 return;
232 list.emplace_back(l->GetName());
233 }
234}
235
236int main(int argc, const char* argv[])
237{
238 Time start;
239
240 gROOT->SetBatch();
241 SetErrorHandler(ErrorHandlerAll);
242
243 Configuration conf(argv[0]);
244 conf.SetPrintUsage(PrintUsage);
245 SetupConfiguration(conf);
246
247 if (!conf.DoParse(argc, argv))
248 return 127;
249
250 // ----------------------------- Evaluate options --------------------------
251 const vector<string> files = conf.Vec<string>("file");
252 const string out = conf.Get<string>("out");
253 const string tree = conf.Get<string>("tree");
254
255 const bool force = conf.Get<bool>("force");
256 const bool append = conf.Get<bool>("append");
257 const bool dryrun = conf.Get<bool>("dry-run");
258 const bool skip = conf.Get<bool>("skip");
259
260 const uint16_t verbose = conf.Get<uint16_t>("verbose");
261 const int64_t first = conf.Get<int64_t>("first");
262 const int64_t max = conf.Get<int64_t>("max");
263 const uint16_t header = conf.Get<uint16_t>("header");
264
265 const bool print_ls = conf.Get<bool>("print-ls");
266 const bool print_branches = conf.Get<bool>("print-branches");
267 const bool print_leaves = conf.Get<bool>("print-leaves");
268
269 const auto _ignore = conf.Vec<string>("ignore");
270 const auto autoalias = conf.Vec<Configuration::Map>("auto-alias");
271
272 if (max && first>=max)
273 cerr << "WARNING: Resource `first` (" << first << ") exceeds `max` (" << max << ")" << endl;
274
275 // -------------------------------------------------------------------------
276
277 /*const*/ Tools::Splitting split(conf);
278
279 if (verbose>0)
280 {
281 cout << "\n-------------------------- Evaluating input ------------------------\n";
282 cout << "Start Time: " << Time::sql << Time(Time::local) << endl;
283 }
284
285 if (verbose>0)
286 cout << "Processing Tree: " << tree << endl;
287
288 TChain c(tree.c_str());
289
290 uint64_t cnt = 0;
291 for (const auto &file : files)
292 {
293 const auto add = c.Add(file.c_str(), 0);
294 if (verbose>0)
295 cout << file << ": " << add << " file(s) added." << endl;
296 cnt += add;
297 }
298
299 if (cnt==0)
300 {
301 cerr << "No files found." << endl;
302 return 1;
303 }
304
305 if (verbose>0)
306 cout << cnt << " file(s) found." << endl;
307
308 if (print_ls)
309 {
310 cout << '\n';
311 c.ls();
312 cout << '\n';
313 }
314
315 c.SetMakeClass(1);
316
317 TObjArray *branches = c.GetListOfBranches();
318 TObjArray *leaves = c.GetListOfLeaves();
319
320 if (print_branches)
321 {
322 cout << '\n';
323 branches->Print();
324 }
325
326 const auto entries = c.GetEntriesFast();
327
328 if (verbose>0)
329 cout << branches->GetEntries() << " branches found." << endl;
330
331 if (print_leaves)
332 {
333 cout << '\n';
334 leaves->Print();
335 }
336 if (verbose>0)
337 {
338 cout << leaves->GetEntries() << " leaves found." << endl;
339 cout << entries << " events found." << endl;
340 }
341
342 // ----------------------------------------------------------------------
343
344 if (verbose>0)
345 cout << "\n-------------------------- Evaluating output -----------------------" << endl;
346
347 vector<FileEntry::Container> vec;
348
349/*
350 const auto fixed = conf.GetWildcardOptions("const.*");
351
352 string where;
353 vector<string> vindex;
354 for (auto it=fixed.cbegin(); it!=fixed.cend(); it++)
355 {
356 const string name = it->substr(6);
357 string val = conf.Get<string>(*it);
358
359 boost::smatch match;
360 if (boost::regex_match(val, match, boost::regex("\\/(.+)(?<!\\\\)\\/(.*)(?<!\\\\)\\/")))
361 {
362 const string reg = match[1];
363 const string fmt = match[2];
364
365 val = boost::regex_replace(file, boost::regex(reg), fmt.empty()?"$0":fmt,
366 boost::regex_constants::format_default|boost::regex_constants::format_no_copy);
367
368 if (verbose>0)
369 {
370 cout << "Regular expression detected for constant column `" << *it << "`\n";
371 cout << "Filename converted with /" << reg << "/ to /" << fmt << "/\n";
372 cout << "Filename: " << file << '\n';
373 cout << "Result: " << val << endl;
374 }
375 }
376
377 if (verbose>2)
378 cout << "\n" << val << " [-const-]";
379 if (verbose>1)
380 cout << " (" << name << ")";
381
382 string sqltype = "INT UNSIGNED";
383
384 for (auto m=sqltypes.cbegin(); m!=sqltypes.cend(); m++)
385 if (m->first==name)
386 sqltype = m->second;
387
388 if (!vec.empty())
389 query += ",\n";
390 query += " `"+name+"` "+sqltype+" NOT NULL COMMENT '--user--'";
391
392 vec.emplace_back(name, val);
393 where += " AND `"+name+"`="+val;
394 vindex.emplace_back(name);
395 }
396 */
397
398 // ------------------------- Setup all branches in tree -------------------
399
400 TIter Next(leaves);
401 TObject *o = 0;
402 while ((o=Next()))
403 {
404 TLeaf *L = c.GetLeaf(o->GetName());
405
406 string name = o->GetName();
407
408 for (auto m=autoalias.cbegin(); m!=autoalias.cend(); m++)
409 name = boost::regex_replace(name, boost::regex(m->first), m->second);
410
411 if (name!=o->GetName())
412 {
413 if (verbose>0)
414 cout << "Auto-alias: " << name << " = " << o->GetName() << endl;
415 if (!c.SetAlias(name.c_str(), o->GetName()))
416 cout << "WARNING - Alias could not be established!" << endl;
417 }
418
419 if (skip)
420 continue;
421
422 if (verbose>2)
423 cout << '\n' << L->GetTitle() << " {" << L->GetTypeName() << "}";
424
425 if (L->GetLenStatic()!=L->GetLen())
426 {
427 if (verbose>2)
428 cout << " (-skipped-)";
429 continue;
430 }
431
432 bool found = false;
433 for (auto b=_ignore.cbegin(); b!=_ignore.cend(); b++)
434 {
435 if (boost::regex_match(o->GetName(), boost::regex(*b)))
436 {
437 found = true;
438 if (verbose>2)
439 cout << " (-ignored-)";
440 break;
441 }
442 }
443 for (auto b=_ignore.cbegin(); b!=_ignore.cend(); b++)
444 {
445 if (boost::regex_match(name.c_str(), boost::regex(*b)))
446 {
447 found = true;
448 if (verbose>2)
449 cout << " (-ignored-)";
450 break;
451 }
452 }
453
454 if (found)
455 continue;
456
457 const string tn = L->GetTypeName();
458
459 const auto it = FileEntry::LUT.root(tn);
460 if (it==FileEntry::LUT.cend())
461 {
462 if (verbose>2)
463 cout << " (-n/a-)";
464 continue;
465 }
466
467 if (verbose==2)
468 cout << '\n' << L->GetTitle() << " {" << L->GetTypeName() << "}";
469
470 if (verbose>1)
471 cout << " (" << name << ")";
472
473 vec.emplace_back(o->GetTitle(), name, it->type, L->GetLenStatic());
474 c.SetBranchAddress(o->GetTitle(), vec.back().ptr);
475 }
476
477 if (verbose>0)
478 {
479 if (skip)
480 cout << "Default columns skipped: ";
481 cout << vec.size() << " default leaf/leaves setup for reading." << endl;
482 }
483
484
485 // ------------------- Configure manual aliases ----------------------------
486
487 const auto valiases = conf.GetWildcardOptions("alias.*");
488 if (verbose>0 && valiases.size()>0)
489 cout << '\n';
490 for (auto it=valiases.cbegin(); it!=valiases.cend(); it++)
491 {
492 const string name = it->substr(6);
493 const string val = conf.Get<string>(*it);
494
495 if (verbose>0)
496 cout << "Alias: " << name << " = " << val << endl;
497
498 if (!c.SetAlias(name.c_str(), val.c_str()))
499 {
500 cerr << "Alias could not be established!" << endl;
501 return 2;
502 }
503 }
504
505 // -------------------------- Configure Selector --------------------------
506
507 vector<string> leaflist;
508 c.SetBranchStatus("*", 1);
509
510 TTreeFormulaManager *manager = new TTreeFormulaManager;
511
512 if (verbose>0)
513 cout << "\nSelector: " << conf.Get<string>("selector") << endl;
514
515 TTreeFormula selector("Selector", conf.Get<string>("selector").c_str(), &c);
516 if (selector.GetNdim()==0)
517 {
518 cerr << "Compilation of Selector failed!" << endl;
519 return 3;
520 }
521 selector.SetQuickLoad(kTRUE);
522 manager->Add(&selector);
523 GetLeaves(leaflist, selector);
524
525 // -------------------- Configure additional columns ----------------------
526
527 vector<TTreeFormula*> formulas;
528
529 const auto vform = conf.GetWildcardOptions("add.*");
530 if (verbose>0 && vform.size()>0)
531 cout << '\n';
532 for (auto it=vform.cbegin(); it!=vform.cend(); it++)
533 {
534 const string name = it->substr(4);
535 const string val = conf.Get<string>(*it);
536
537 if (verbose>0)
538 cout << "Adding column: " << name << " = " << val << endl;
539
540 TTreeFormula *form = new TTreeFormula(name.c_str(), val.c_str(), &c);
541 if (form->GetNdim()==0)
542 {
543 cerr << "Compilation of Column failed!" << endl;
544 return 4;
545 }
546 form->SetQuickLoad(kTRUE);
547 formulas.emplace_back(form);
548 manager->Add(form);
549 GetLeaves(leaflist, *form);
550 }
551 manager->Sync();
552
553 if (verbose>0)
554 cout << '\n' << formulas.size() << " additional columns setup for writing." << endl;
555
556 // --------------------- Setup all branches in formulas -------------------
557
558 for (auto l=leaflist.cbegin(); l!=leaflist.cend(); l++)
559 {
560 // Branch address already set
561 if (c.GetBranch(l->c_str())->GetAddress())
562 continue;
563
564 TLeaf *L = c.GetLeaf(l->c_str());
565
566 if (verbose>2)
567 cout << '\n' << L->GetTitle() << " {" << L->GetTypeName() << "}";
568
569 if (L->GetLenStatic()!=L->GetLen())
570 {
571 if (verbose>2)
572 cout << " (-skipped-)";
573 continue;
574 }
575
576 const string tn = L->GetTypeName();
577
578 const auto it = FileEntry::LUT.root(tn);
579 if (it==FileEntry::LUT.cend())
580 {
581 if (verbose>2)
582 cout << " (-n/a-)";
583 continue;
584 }
585
586 if (verbose==2)
587 cout << '\n' << L->GetTitle() << " {" << L->GetTypeName() << "}";
588
589 if (verbose>1)
590 cout << " (" << *l << ")";
591
592 vec.emplace_back(l->c_str(), l->c_str(), it->type, L->GetLenStatic());
593 c.SetBranchAddress(l->c_str(), vec.back().ptr);
594 }
595 if (verbose>1)
596 cout << '\n';
597
598 // ------------------------- Enable branch reading ------------------------
599
600 UInt_t datatype = 0;
601 const bool has_datatype = c.SetBranchAddress("DataType.fVal", &datatype) >= 0;
602
603 // Seting up branch status (must be after all SetBranchAddress)
604 c.SetBranchStatus("*", 0);
605 for (auto v=vec.cbegin(); v!=vec.cend(); v++)
606 if (v->type!=FileEntry::kConst)
607 c.SetBranchStatus(v->branch.c_str(), 1);
608
609 if (has_datatype)
610 {
611 c.SetBranchStatus("DataType.fVal", 1);
612 if (verbose>0)
613 cout << "Rows with DataType.fVal!=1 will be skipped." << endl;
614 }
615
616 // -------------------------------------------------------------------------
617
618 if (verbose>0)
619 {
620 cout << '\n';
621 split.print();
622 }
623
624 if (dryrun)
625 {
626 cout << "\nDry run: file output skipped!" << endl;
627 return 0;
628 }
629
630 if (verbose>0)
631 cout << "\n-------------------------- Converting file -------------------------" << endl;
632
633 vector<ofstream> outfiles;
634
635 if (split.empty())
636 {
637 TString path(out.c_str());
638 const int rc = CheckFile(path, force, verbose);
639 if (rc>0)
640 return rc;
641
642 outfiles.emplace_back(path.Data(), append ? ios::app : ios::trunc);
643 if (rc==-1 || (force && rc==0 && !append))
644 WriteHeader(outfiles.back(), vec, formulas, skip, header);
645 }
646 else
647 {
648 for (size_t i=0; i<split.size(); i++)
649 {
650 TString path(out.c_str());
651 path += "-";
652 path += i;
653
654 const int rc = CheckFile(path, force, verbose);
655 if (rc>0)
656 return rc;
657 outfiles.emplace_back(path.Data(), append ? ios::app : ios::trunc);
658 if (rc==-1 || (force && rc==0 && !append))
659 WriteHeader(outfiles.back(), vec, formulas, skip, header);
660 }
661 }
662
663 // ---------------------------- Write Body --------------------------------
664 size_t count = 0;
665 vector<size_t> ncount(split.empty()?1:split.size());
666
667 auto itree = c.GetTreeNumber();
668
669 const size_t num = max>0 && (max-first)<entries ? (max-first) : entries;
670 for (size_t j=first; j<num; j++)
671 {
672 c.GetEntry(j);
673 if (has_datatype && datatype!=1)
674 continue;
675
676 if (itree != c.GetTreeNumber())
677 {
678 manager->UpdateFormulaLeaves();
679 itree = c.GetTreeNumber();
680 }
681
682 if (selector.GetNdim() && selector.EvalInstance(0)<=0)
683 continue;
684
685 const size_t index = split.index(count++);
686 ncount[index]++;
687
688 vector<string> join;
689
690 if (!skip)
691 {
692 for (auto v=vec.cbegin(); v!=vec.cend(); v++)
693 {
694 const size_t N = v->num;
695 for (size_t i=0; i<N; i++)
696 join.emplace_back(v->fmt(i));
697 }
698 }
699
700 for (auto v=formulas.cbegin(); v!=formulas.cend(); v++)
701 join.emplace_back(to_string((*v)->EvalInstance(0)));
702
703 outfiles[index] << boost::join(join, " ") << "\n";
704 }
705
706 if (verbose>0)
707 {
708 cout << "\nTotal: N=" << count << " out of " << num << " row(s) written [N=" << first << ".." << num-1 << "]." << endl;
709 for (int i=0; i<split.size(); i++)
710 cout << "File " << i << ": nrows=" << ncount[i] << '\n';
711 cout << '\n';
712 }
713
714 if (verbose>0)
715 {
716 cout << "Total execution time: " << Time().UnixTime()-start.UnixTime() << "s.\n";
717 cout << "Success!\n" << endl;
718 }
719 return 0;
720}
Note: See TracBrowser for help on using the repository browser.