-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdbf2csv.pl
More file actions
executable file
·1553 lines (1402 loc) · 62.5 KB
/
Copy pathdbf2csv.pl
File metadata and controls
executable file
·1553 lines (1402 loc) · 62.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/perl
# DBF2CSV.PL -- a program to convert .DBF files to .CSV or .json format
# (Written in Perl 4.036, and compatible with Perl 5.)
#
# This program is uncopyrighted, so do with it whatever you wish.
# By Dave Burton, Burton Systems Software, POB 4157, Cary, NC 27519-4157.
# email: http://www.burtonsys.com/email/
# The latest version of DBF2CSV can always be found on the Burton
# Systems Software web site, in the "downloads" area:
# http://www.burtonsys.com/
# =>keyflag<=-- "%v, %d"
$ver_ordinal_and_date = "41, 03-Dec-16";
# the TLIB Version Control version number and last-modified date
######## HISTORY ########
#
# Dbf2csv v1, 12/22/2000 - Convert .DBF files to .CSV format.
# The "-p" option selects interactive mode, for use with Perl4w.exe,
# to make a Windows program.
#
# Dbf2csv v2, 4/14/2003 - handles some ideosyncratic .dbf files with
# non-standard headers, which confused v1.
#
# Dbf2csv v3 (unreleased) - added support for fields > 255 characters
# thanks to Jeff Price, and for big-endian machines (for John McVeagh).
#
# v1 was modified (commented, corrected & enhanced to handle accented
# characters) by Jacky Bruno <jacky.bruno[at]free.fr> collège de Villeneuve
# sur Yonne 89500, in april 2003. His version was included in some of
# the dbf2csv.zip distributions as dbf2csv_accentued_characters.pl.
#
# Dbf2csv v4, 6/27/2003 - the result of merging most of Jacky's changes
# into the standard version of DBF2CSV.PL, plus a few more improvements.
# Also, changes '~' characters to blanks.
# [Shameless advertisement: of course I used the TLIB Version Control
# "migrate" command to do the merge and create v4. See our web site,
# http://www.burtonsys.com/ -DAB]
#
# Dbf2csv v5, 1/24/2004 - adds the ability to properly handle FoxPro's
# 'I' fields, which are 4-byte little-endian binary integers (signed,
# I hope!). Also, tries to tell you what program created the database,
# based on the version number. Also, no longer changes '~'s to blanks.
#
# Dbf2csv v6, 5/7/2004 - by David Marín <dmarin-NO-SPAM@dyr.es>
# Handle almost all PC850 accented characters, not just French ones
#
# Dbf2csv v7, 7/19/2005 - 'F' fields now fully supported, and '+'
# fields now might work (untested, supported with a warning). -DAB
#
# Dbf2csv v7.1-beta 10/9/2010 - Unsupported fields converted to hex. -DAB
#
# Dbf2csv v8, 2/20/2011 - 'B' fields (FoxPro 8-byte double-precsion)
# now work and negative values in 'I" fields now work correctly on
# small-endian machines (but still not on big-endian machines, sorry!).
# Also, added the "-j" option, to output a .json file instead of a .csv
# file (EXPERIMENTAL / lightly tested). -DAB
#
# Dbf2csv v9, 5/22/2012 - 'T' fields (FoxPro 8-byte date+time) are now
# supported. -DAB
#
# Dbf2csv v10, 11/15/2012 - Fixed detection of (unsupported) dBase 7
# files. -DAB
#
# Dbf2csv v11, 12/3/2016 - Enabled escape-coding of special characters.
# -DAB
#
#########################
$version = "v11";
# This program is uncopyrighted, so it can be modified by anyone who
# wants to. But, out of courtesy, please add your own name and what
# you did to the history, and do not remove the previous history.
# -DAB
# Here are some descriptions of the .dbf file format:
# http://www.clicketyclick.dk/databases/xbase/format/
# http://www.clicketyclick.dk/databases/xbase/format/data_types.html
# http://web.archive.org/web/20080501103856/http://www.klaban.torun.pl/prog/pg2xbase/contrib/dbf-format.html
# http://support.microsoft.com/kb/98743 or http://www.webcitation.org/67szzmtLQ (was http://support.microsoft.com/support/kb/articles/q98/7/43.asp)
# http://web.archive.org/web/20060821235500/http://community.borland.com/article/0,1410,15838,00.html
# http://www.dbase.com/KnowledgeBase/int/db7_file_fmt.htm or http://www.webcitation.org/67t06OStF
# Jacky Bruno's additional comments... (but I updated the line numbers -DAB)
# The script structure is this one:
# lines 1509 to the end : main program :
# - it reads command line: if no parameters, then it shows help
# - treat flags if there are some (d, p)
# - reads the first file name given and give it to the do_a_file function
# - do the same with other command line given files
# lines 1333 to 1364 : do_a_file function (the name is clear) :
# - verify the validity of the file (ending with .dbf)
# - buids the .csv file name from the .dbf file name given
# - calls the cvt1file function by giving her the 2 file names
# - get the records's number and shows informations:
# input file output file treated records number
# lines 610 to 1326 : cvt1file function :
# - as his name tells, do the conversion job of input file
# - write to screen informations of file beginning and field names
# - save output file :
# first field name, then each record
# In the output file, fields will be separated by $separe
# $separe=";";
$separe=",";
$| = 1; # predefined variable. If <> 0 then each print to the console
# will immediatly be displayed, instead of buffered.
$debugmode = 0; # set to 1 via '-d' for debug prints
$prompt = 0; # set to 1 via '-p' for special interactive mode, for use with perl4w.exe
$jsonmode = 0; # set to 1 via '-j' to output a JSON file instead of CSV
$escape_codes = -1; # set to 1 via '-e' to translate CR, LF, etc. to "\r", "\n", etc. respectively
# or set to 2 via '-e2' to translate " to "" (not for json!)
# default is '-e0' (dumb/simple mode) for CSV, or '-e1' for json
$progversion_shown = 0;
# display program name and version (but only once!)
sub show_progversion {
if (!$progversion_shown) {
print "DBF2CSV $version -- Convert .DBF file to .CSV (comma-separated) format\n";
$progversion_shown = 1;
}
}
# display dbf2csv.pl version number (you might want to comment this out)
&show_progversion;
# Which version of Perl are they using?
$perlver = "3 or earlier";
if ($] =~ /\$\$Revision\:\s*([0-9.]+)\s/) {
$perlver = $1; # probably 4.something
} elsif ($] =~ /([0-9][0-9.]*)/) {
$perlver = $1; # probably 5.something or 6.something
}
print "You are using Perl version $perlver\n";
# is this a big-endian machine?
$big_endian = 0;
$tst = pack("S",513);
$tst_big_endian = unpack("n",$tst);
$tst_little_endian = unpack("v",$tst);
if ((513==$tst_big_endian) && (258==$tst_little_endian)) {
# this is a big endian machine
$big_endian = 1
}
# Does this version of Perl support IEEE-754 8-byte double-precision (Foxpro 'B')?
$perl_supports_IEEE754_doubles = 0;
$tstIEEE = "\x00\x00\x00\x00\x80\x05\xD1\x40";
$tst2 = unpack("d",$tstIEEE);
$tst3 = sprintf("%10.4f", $tst2);
if ($tst3 eq '17430.0000') {
$perl_supports_IEEE754_doubles = 1;
}
$warned_about_B_field = 0; # set to 1 when we issue a warning about inability to handle Foxpro 'B' fields (so we won't repeat the warning)
# Perl pack/unpack format representing the structure of the first
# 32 bytes in a .DBF file:
$DBF_header_fmt = "C" . # version number at offset
"CCC" . # YY, MM, DD (one byte each)
"L" . # Number of records in file
"S" . # Length of header structure
"S" . # Length of each record
"a20"; # 20 bytes that we don't care about
# Perl pack/unpack format representing the structure of each field descriptor
# (the 2nd-Nth 32-byte chunk):
$DBF_field_desc_fmt = "A11" . # Field name in 0-terminated ASCII
"a" . # Field type in ASCII
"L" . # Field address in memory (unused)
"C" . # Field length (binary) \___/ these 2 bytes can also be a 2-byte field length,
"C" . # Decimal count (binary) / \ 1-65535, for field type 'C' in Foxbase & Clipper.
"C" . # Field flags (FoxPro/FoxBase only)
"a1" . # reserved
"C" . # Work area ID
"a2" . # reserved
"C" . # Flag for SET FIELDS
"a7" . # reserved
"A"; # Index field flag
# For the meanings of the template letters, see Perl documentation
# (e.g., on Linux do 'man perlfunc' then read 'pack')
# Unfortunately, the "v" & "V" template characters (Vax-byte-order integers)
# are poorly documented: the Perl docs suggest that they are signed, but they
# seem to be unsigned on my x86 machines. The "S" & "L" template characters
# (machine-order unsigned integers) won't work right on big-endian machines.
# So we do the best we can: on big endian machines we change the 'S' template
# characters to 'v' and the 'L' template character to 'V'. That'll work 99.9%
# of the time -- i.e., as long as the record length doesn't exceed 32K, or
# the v and V templates are unsigned.
# Thanks to John McVeagh (who uses AIX) for inspiring this.
if ($big_endian) {
$DBF_header_fmt =~ s/LSS/Vvv/;
print "Note: This is a big-endian machine. Adjusting template.\n";
}
# $cvt_failed is a side-effect result of &cvt1file.
$cvt_failed = 0; # will be set to 1 iff cvt1file failed, or 0 for success
# handy constant
$zerobyte = "\0"; # same as pack("c",0), or in Perl 5 it could also be chr(0)
# use the '-ta' or '-tu' option to adjust $translate and $garde_accent
# By Jacky Bruno...
# The file can have accentued characters coded in DOS pc style (where, for example "é"
# is coded "82h") or coded in ANSI style (linux or Windows) ( "é" is coded "E9h")
# If codage is already ANSI, no need to re-code it: put $translate to 0
# If codage is pc, you can choose to re-code or let codage the way it is.
# Accent codage change? codage pc --> codage this way:
# 1 -> yes, let's change codage (another number is possible)
# 0 -> no, let's keep the codage the way it is in the original file
$translate=0;
# If codage is changed ($translate=1), do we keep accentued characters?
# Translation keeping or not accentued characters
# 1 -> keeping accentued characters (or another number)
# 0 -> don't keep accentued characters
$garde_accent=1; #In french: keep = garde
# Conversion tables codage pc <--> codage ansi for accentued characters
# They can be completed regarding the "correspondances"
# (every character must respect the same order in every codage)
$code_pc="\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a".
"\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94".
"\x95\x96\x97\x98\x99\x9a\xa0\xa1\xa2\xa3".
"\xa4\xa5\xb5\xb6\xb7\xc6\xc7\xd2\xd3\xd4".
"\xd5\xd6\xd7\xd8\xe0\xe1\xe2\xe3\xe4\xe5".
"\xe9\xea\xeb\xec\xed";
$code_ansi="üéâäàåçêëè".
"ïîìÄÅÉæÆôö".
"òûùÿÖÜáíóú".
"ñÑÁÂÀãÃÊËÈ".
"iÍÎÏÓßÔÒõÕ".
"ÚÛÙýÝ";
$code_brut="ueaaaaceee".
"iiiAAEeEoo".
"ouuyOUaiou".
"nNAAAaAEEE".
"iIIIOSOOoO".
"UUUyY";
$silent = 0; # a flag we sometimes set to avoid redundant warning messages
####################################
### end of global variables
####################################
# Given the version number found in the header of a .dbf file, return our
# best guess for the name of the product which created the .dbf file
sub productName {
local($product);
if (2 == $verNum) {
$product = "(FoxBase)";
} elsif (3 == $verNum) {
$product = "(File without DBT)";
} elsif (4 == $verNum) {
$product = "(dBASE IV w/o memo file)";
} elsif (5 == $verNum) {
$product = "(dBASE V w/o memo file)";
} elsif (0x30 == $verNum) { # 48
$product = "(Visual FoxPro)";
} elsif (0x31 == $verNum) { # 49
$product = "(Visual FoxPro w/ AutoIncrement field)";
} elsif (0x7B == $verNum) { # 123
$product = "(dBASE IV w/ memo file)";
} elsif (0x83 == $verNum) { # 131
$product = "(File w/ DBT, or dBASE III+ w/ memo file)";
} elsif (0x8B == $verNum) { # 139
$product = "(dBASE III or IV w/ memo file)"; # microsoft says it is dBase III, but http://www.e-bachmann.dk/docs/xbase.htm says it is dBase IV
} elsif (0x8E == $verNum) { # 142
$product = "(dBASE IV w/ SQL table )";
} elsif (0xE5 == $verNum) { # 229
$product = "(Clipper SIX w/ SMT memo file)";
} elsif (0xE6 == $verNum) { # 230
$product = "\nWarning: Encrypted Clipper SIX database, probably can't be handled\n";
} elsif (0xF5 == $verNum) { # 245
$product = "(FoxPro w/ memo file)";
} elsif (0xFB == $verNum) { # 251
$product = "(FoxPro ???)";
} else {
$product = "(unknown xBase database program [perhaps Alpha4/Alpha5, dBFast, DataBoss, CodeBase, or Lotus Approach], please contact Dave Burton at http:\\\\www.burtonsys.com\\email\\)";
}
return $product;
} #productName
# globals used by &fetch_memo
$current_dbf_file = '';
$current_memo_file = '';
$current_memo_file_is_open = 0;
$current_memo_file_size = 0;
$current_memo_file_blocksize = 0; # will be changed to 512 or 64 or whatever
$null_memo_count = 0;
$real_memo_count = 0;
# Retrieve a memo from a FoxPro .FPT memo file. Input is the block number.
# MEMOFILE must already be open, and globals $current_memo_file* must already
# be set.
sub fetch_fpt_memo {
local($blocknum) = shift;
local($fbuf) = '';
local($fileofs) = $blocknum * $current_memo_file_blocksize;
if (($fileofs+8) >= $current_memo_file_size) {
printf "ERROR: block $blocknum (offset $fileofs) of '$current_memo_file' is past EOF.\n";
return '';
}
seek( MEMOFILE, $fileofs, 0 );
if (!sysread(MEMOFILE, $fbuf, 8)) {
print "ERROR: Could not read '$current_memo_file' at ofs=$fileofs, $!\n";
return '';
}
local($memotype, $memolen) = unpack( "NN", $fbuf );
# 'N' is 4-byte big-endian integer
if (($memotype < 0) || ($memotype > 2)) {
print "Warning: unrecognized Memo type $memotype for Memo block $blocknum\n" .
"(offset $fileofs) in '$current_memo_file'.\n";
}
if ($memolen < 0) {
print "Warning: bad Memo file '$current_memo_file', memo block $blocknum has\n" .
"negative size.\n";
}
if ($memolen <= 0) {
return '';
}
if (($memolen + $fileofs + 8) > $current_memo_file_size) {
local($newlen) = $current_memo_file_size - ($fileofs + 8);
print "Warning: bad Memo file '$current_memo_file', memo block $blocknum is\n" .
"incomplete (truncated from $memolen to $newlen bytes)\n";
$memolen = $newlen;
}
$fbuf = '';
if (!sysread(MEMOFILE, $fbuf, $memolen)) {
print "ERROR: could not read $memolen-byte memo from '$current_memo_file'\n" .
"at block $blocknum (offset $fileofs)\n";
}
return $fbuf;
} #fetch_fpt_memo
# read MEMOFILE (assumed to be an open .DBT file), and determine the BLOCKSIZE
sub fetch_dbt_blocksize {
local($mbuf, $blksiz);
$blksiz = 0;
seek( MEMOFILE, 16, 0 );
$mbuf = "\1";
if (!sysread(MEMOFILE, $mbuf, 1)) {
print "ERROR: Could not read '$current_memo_file' at ofs=16, $!\n";
} else {
if ($mbuf eq $zerobyte) {
# dBase IV, supports variable BLOCKSIZE
seek( MEMOFILE, 20, 0 );
$mbuf = "\0\0";
sysread(MEMOFILE, $mbuf, 1);
$blksiz = unpack( "v", $mbuf );
# "v" means 2-byte little-endian integer
if (1 == $current_memo_file_blocksize) {
# dBase III format memo file created by dBase IV ?
$blksiz = 512;
} elsif ($current_memo_file_blocksize < 0) {
# fix overflow for blocksize 32768 (maybe not possible anyhow)
$blksiz += 65536;
}
if (! $blksiz) {
print "ERROR: '$current_memo_file' is not a valid dBase IV memo file (no blocksize\n" .
"found at file offset 20).\n";
}
} elsif ($mbuf eq "\3") {
# dBase III+, always uses 512-byte BLOCKSIZE
$blksiz = 512;
} else {
# unrecognized file format
print "ERROR: '$current_memo_file' is an unrecognized xBase memo file format\n";
}
}
return $blksiz;
} #fetch_dbt_blocksize
# read MEMOFILE (assumed to be an open .FPT file), and determine the BLOCKSIZE
sub fetch_fpt_blocksize {
local($mbuf, $blksiz);
$blksiz = 0;
seek( MEMOFILE, 6, 0 );
$mbuf = "\0\0";
if (!sysread(MEMOFILE, $mbuf, 2)) {
print "ERROR: Could not read '$current_memo_file' at ofs=6, $!\n";
return 0;
} else {
$blksiz = unpack( "n", $mbuf );
# "n" means 2-byte big-endian integer
if (! $blksiz) {
print "ERROR: '$current_memo_file' is not a valid FoxPro memo file (no blocksize\n" .
"found at file offset 6).\n";
}
}
return $blksiz;
} #fetch_fpt_blocksize
# Input is name of .dbf file (not the memo file!), and the memo block number.
# Returns the memo in a string.
# The memo file can be a .dbt file or a .fpt file. This subroutine would look
# for both (except that .dbt file support isn't yet implemented).
# If the .dbf file name is '', then current memo file is closed, '' is
# returned, and nothing else is done.
# If the block number is zero, then '' is always returned.
# If .dbf file name has changed since last time, the old memo file is closed,
# and the new one is opened.
# If the memo file can't be read, then a string is returned containing the
# block number preceeded by '#'.
sub fetch_memo {
local( $dbf_file, $blocknum ) = @_;
local( $result ) = '';
local( $mbuf );
if (($dbf_file ne $current_dbf_file) && $current_memo_file_is_open) {
if ($debugmode) {
print "dbg: Closing '$current_memo_file' (memo file for '$current_dbf_file').\n";
}
close MEMOFILE;
$current_memo_file_is_open = 0;
$current_memo_file_size = 0;
$current_memo_file_blocksize = 0;
$null_memo_count = 0;
$real_memo_count = 0;
}
if (('' eq $dbf_file) || (0 == $blocknum)) {
return '';
}
if (($dbf_file ne $current_dbf_file)) {
$current_dbf_file = $dbf_file;
$current_memo_file = '';
# figure out name of memo file
$fpt_file = $dbf_file;
$fpt_file =~ s/\.DBF$/.FPT/;
$fpt_file =~ s/\.dbf$/.fpt/i;
$dbt_file = $dbf_file;
$dbt_file =~ s/\.DBF$/.DBT/;
$dbt_file =~ s/\.dbf$/.dbt/i;
if ($fpt_file eq $dbt_file) {
printf "ERR: '$dbf_file' is not the name of a .DBF file\n";
return '?';
}
if (-f $fpt_file) {
$current_memo_file = $fpt_file;
} elsif (-f $dbt_file) {
$current_memo_file = $dbt_file;
}
# open memo file
if ('' ne $current_memo_file) {
if (!open( MEMOFILE, $current_memo_file )) {
print "ERROR: Could not open '$current_memo_file', $!\n";
} else {
$current_memo_file_is_open = 1;
}
} else {
print "ERROR: Memo file $dbt_file or $fpt_file not found.\n";
}
# next figure out the SET BLOCKSIZE setting:
if ($current_memo_file_is_open) {
# We must read the memo file in binary mode (avoid translations of end-lines)
binmode MEMOFILE;
# get size of memo file
$current_memo_file_size = -s MEMOFILE;
if ($current_memo_file eq $dbt_file) {
# .DBT file
$current_memo_file_blocksize = &fetch_dbt_blocksize;
} else {
# .FPT file
$current_memo_file_blocksize = &fetch_fpt_blocksize;
}
if ($current_memo_file_blocksize < 0) {
# fix overflow for blocksize 32768 (maybe not possible anyhow)
$current_memo_file_blocksize += 65536;
}
if (0 == $current_memo_file_blocksize) {
close MEMOFILE;
$current_memo_file_is_open = 0;
# error message was displayed by fetch_dbt_blocksize or fetch_fpt_blocksize
}
if ($current_memo_file_is_open) {
print "Opening memo file '$current_memo_file' for '$dbf_file', block size = $current_memo_file_blocksize.\n";
}
}
if ($current_memo_file =~ /\.dbt$/) {
print "ERROR: This program doesn't yet understand .DBT (memo) files. If you can\n" .
"furnish me with some test files, I might be able to add support for you.\n" .
"Email me via http\://www.burtonsys.com/email/\n";
if ($current_memo_file_is_open) {
close MEMOFILE;
$current_memo_file_is_open = 0;
}
}
if (! $current_memo_file_is_open) {
print "Warning: memo fields will be converted as block numbers, rather than the\n" .
"actual memo text.\n";
}
}
if (! $current_memo_file_is_open) {
return '#' . $blocknum;
}
# Fetch the memo from the memo file
if ($current_memo_file =~ /\.dbt$/) {
# not currently implemented
$result = &fetch_dbt_memo( $blocknum );
} else {
$result = &fetch_fpt_memo( $blocknum );
}
if ('' eq $result) {
$null_memo_count++;
} else {
$real_memo_count++;
}
return $result;
} #fetch_memo
# Jdate2Cal converts a 4-byte integer Julian Date to YYYY-MM-DD string.
# The code was cribbed (with minor modifications) from sub CalDate in CalDate.pm from
# http://sourceforge.net/projects/jday/files/jday/2.4/jday-2.4.tar.gz (which is copyrighted code), and
# used in this uncopyrighted program by the gracious permission of author Hiram Clawson (jday at hiram.ws):
# "Good Afternoon David: Please feel free to use the code snippet as you desire. --Hiram" 5/23/2012
sub Jdate2Cal {
local($jd, $ka, $ialp, $kb, $kc, $kd, $ke, $day, $month, $year, $result);
$jd = shift; # integer julian date
$jd = int($jd);
$ka = $jd;
if ( $jd >= 2299161 ) {
$ialp = int(( $jd - 1867216.25 ) / ( 36524.25 ));
$ka = int($jd + 1 + $ialp - ( $ialp >> 2 ));
}
$kb = int($ka + 1524);
$kc = int(( $kb - 122.1 ) / 365.25);
$kd = int($kc * 365.25);
$ke = int(( $kb - $kd ) / 30.6001);
$day = $kb - $kd - int( $ke * 30.6001 );
if ($ke > 13) {
$month = int($ke - 13);
} else {
$month = int($ke - 1);
}
if ( ($month == 2) && ($day > 28) ) {
$day = 29;
}
if (($month == 2) && ($day == 29) && ($ke == 3)) {
$year = int($kc - 4716);
} elsif ($month > 2) {
$year = int($kc - 4716);
} else {
$year = int($kc - 4715);
}
# local($dayofweek) = int(($jd + 1) % 7);
# local($dayofyear)
# if ( $year == (($year >> 2) << 2) ) {
# $dayofyear =
# int( ( 275 * $month ) / 9)
# - int(($month + 9) / 12)
# + $day - 30;
# } else {
# $dayofyear =
# int( ( 275 * $month) / 9)
# - ((($month + 9) / 12) << 1)
# + $day - 30;
# }
# $result = $year . '-' . $month . '-' . $day;
$result = sprintf( "%04d-%02d-%02d", $year, $month, $day );
return $result;
} # Jdate2Cal
# sanity tests
$tmp1 = &Jdate2Cal(2455958);
if ($tmp1 ne "2012-01-31") {
print "ERROR: Jdate2Cal(2455958) = '" . $tmp1 . "' but it should be '2012-01-31'\n";
}
$tmp1 = &Jdate2Cal(2299161);
if ($tmp1 ne "1582-10-15") {
print "ERROR: Jdate2Cal(2455958) = '" . $tmp1 . "' but it should be '1582-10-15'\n";
}
# Read infile (name of a .DBF file) and create outfile (name of a .CSV file).
sub cvt1file {
local( $infile, $outfile ) = @_;
local( $recnum ) = 0;
local( $skipped ) = 0;
local( $offset_of_0 );
local( $buf ) = "";
local( $reclens_look_right ) = 0;
local( $offsets_match_fieldlens );
local( $i );
local( @tmp, $tmp, $tmp1 );
local( $has_memo_fields ) = 0; # are there any 'M' fields (fields that use the memo file)?
local( $date, $time, $hr, $min, $sec );
$cvt_failed = 0; # side-effect result, 0 or 1
# open dbf file
if (!open( DBF, $infile )) {
&show_progversion;
print "ERROR: Could not open '$infile', $!\n";
$cvt_failed = 1;
return 0; # no records converted
}
# We must read the input .dbf file in binary mode (avoid translations of end-lines)
binmode DBF;
# get size of .dbf input file
$DBF_file_size = -s DBF;
# Remove old output file if it exists
unlink $outfile; # mostly in case outfile is on a buggy MARS_NWE volume, so we don't get trailing junk in the output file if it already existed and was bigger than the new output file
if (!sysread(DBF, $buf, 32)) {
&show_progversion;
print "ERROR: Could not read first 32 bytes from '$infile', $!\n";
$cvt_failed = 1;
return 0; # no records converted
}
# Unpack the file header fields from the first 32 bytes of the .dbf file
# The $DBF_header_fmt template is defined above
( $verNum, $yy, $mm, $dd, $numrecs, $hdrLen, $recLen ) = unpack( $DBF_header_fmt, $buf );
$might_be_dBase7 = (4 == (7 & $verNum));
if ($debugmode) {
print "dbg[1]: might_be_dBase7 = $might_be_dBase7\n";
}
if (($hdrLen % 48) != 21) {
$might_be_dBase7 = 0;
}
if ($debugmode) {
print "dbg[2]: might_be_dBase7 = $might_be_dBase7, (hdrLen%48)=" . ($hdrLen % 48) . "\n";
}
print "version=$verNum ";
$product = &productName( $verNum );
if ($might_be_dBase7) {
$product = "(dBASE IV w/o memo file, or perhaps dBase 7)"
}
if ("" ne $product) {
print "$product ";
}
if ($yy < 78) { # The first .dbf file was created 1/29/1978, so it can't be older than that
# Microsoft leaves off the 100, so fix it:
$yy += 100;
}
printf " yyyy/mm/dd=%04d/%02d/%02d\n", ($yy+1900),$mm,$dd;
# printf " jj/mm/aa=%02d/%02d/%02d\n", $dd,$mm,$yy; # -- French (by Jacky Bruno) --
print "numrecs=$numrecs";
# print "nombreEnregValides=$numrecs"; # -- French (by Jacky Bruno) --
$calculated_numrecs = int(($DBF_file_size - $hdrLen) / $recLen);
print ", calculated numrecs=$calculated_numrecs.\n";
if ($numrecs != $calculated_numrecs) {
print "ERROR: numcres from header unequal to calculated number of records.\n";
if ($calculated_numrecs < $numrecs) {
print "$infile might be incomplete.\n";
} else {
printf "Final %d records are suspect.\n", $calculated_numrecs-$numrecs;
}
}
print "hdrLen=$hdrLen ";
# print "hdrLong=$hdrLen "; # -- French (by Jacky Bruno) --
print "recLen=$recLen ";
# print "enregLong=$recLen "; # -- French (by Jacky Bruno) --
$numfields = int(($hdrLen - 1) / 32) - 1;
if ($might_be_dBase7) {
$db7numfields = int(($hdrLen - 69) / 48);
print " numfields = $numfields per record (or $db7numfields per record if this is a dBase 7 file).\n";
} else {
print " numfields = $numfields per record.\n";
}
# print "nombreChamps=$numfields (by record)\n"; # -- French (by Jacky Bruno) --
$extra_hdr_bytes = ($hdrLen - (1+(($numfields+1)*32)));
if ($extra_hdr_bytes != 0) {
if ((48 == $verNum) && (7 == $extra_hdr_bytes)) { # Visual FoxPro idiosyncracy
print "Visual FoxPro idiosyncracy: 7 extra header bytes (ignored)\n";
} else {
print "Warning: non-standard .dbf file format, header contains $extra_hdr_bytes extra byte(s).\n";
}
}
if ($might_be_dBase7) {
# Looks like dBase 7
# dBase 7 is a very different format, with 48-byte (instead of 32-byte) field
# definition headers; see http://www.dbase.com/KnowledgeBase/int/db7_file_fmt.htm
# The header of a dBase 7 file has 36 extra header bytes (mostly the Language
# driver name), plus 16 extra bytes per field descriptor, plus the Field
# Properties Structure (which I think is at least 16 bytes)
print "Warning: $infile might be a dBase 7 file! This tool does not support dBase 7.\n";
}
$extra_file_bytes = ($DBF_file_size - ($hdrLen + ($calculated_numrecs * $recLen)));
if ($extra_file_bytes > 0) {
print "Warning: $infile contains $extra_file_bytes extra byte(s) at the end (ignored).\n";
}
# $recfmt will be the unpacking template for each record.
# This template will be build by reading field's definitions
# (32 bytes per field starting at the 33rd byte of the file)
$recfmt = "A"; # first byte of each record is the "deleted" indicator byte (normally blank)
# We will build arrays containing fields caracteristics
# (name, type, width, offset).
# The [0] array entries are for the "deleted" indicator byte:
$fld_nam[0] = '';
$fld_ofs[0] = 0;
$fld_len[0] = 1;
$fld_typ[0] = 'C';
$fld_flg[0] = 0;
$fld_supported[0] = 1;
$running_offset = 1; # 1, not 0, because the "deleted" indicator is 1 byte
# read all the field definition headers (32 bytes each):
for ($i=1; $i <= $numfields; $i++) {
if (!sysread(DBF, $buf, 32)) {
print "ERROR: Could not read field definition header for field $i from '$infile', $!\n";
$cvt_failed = 1;
return 0; # exit with error
}
# Unpack field definition using $DBF_field_desc_fmt template (we keep
# only the first 5 fields):
( $fldName, $fldType, $fldOffset, $fldLen, $decCnt, $fldFlags ) = unpack( $DBF_field_desc_fmt, $buf );
# I don't know why the dumb A11 format doesn't strip the garbage after
# the 0-byte, but it doesn't. The Perl documentation says, "When
# unpacking, 'A' strips trailing spaces and nulls," but that apparently
# doesn't mean that it truncates at the first null byte. We could use
# "Z11" instead of "A11" if we didn't care about Perl 4 compatibility.
# Most .dbf files don't have trailing garbage after the 0-byte, anyhow,
# but some do. This is for those .dbf files.
$offset_of_0 = index($fldName, $zerobyte);
if (-1 != $offset_of_0) {
$fldName = substr( $fldName, 0, $offset_of_0 );
}
# Some xBase variants (Clipper, Foxbase, perhaps others) permit
# character data fields larger than 255 characters, using the
# "Decimal Count" field as a high length byte. (Thanks to Jeff
# Price <jeff.price@rocketmail.com.nospam> for telling me this.)
if (($decCnt > 0) && ('C' eq $fldType) && ($recLen >= (256 * $decCnt))) {
$fldLen += (256 * $decCnt);
}
$fldName =~ s/\r/ /g; # change "\rtastrade.d" to " tastrade.d" to avoid messing up display
if ($debugmode) {
printf "%3d: %-10s type='%s' offset=%d fldLen=%d, fldFlags=0x%02x\n",
$i, $fldName, $fldType, $fldOffset, $fldLen, $fldFlags;
}
$fld_nam[$i] = $fldName;
$fld_ofs[$i] = $fldOffset;
$fld_len[$i] = $fldLen;
$fld_typ[$i] = $fldType;
$fld_flg[$i] = $fldFlags;
# Add another field to the template, type 'A' (text completed by spaces) with $fldLen width
if ((('I' eq $fldType) || ('+' eq $fldType)) && (4 == $fldLen)) {
# Two special cases:
# 'I' is FoxPro or dBase 7 binary 4-byte integer. For FoxPro, it is known to be little-endian; I dunno about dBase 7. For dBase, it is known to be signed; I presume that is also true for FoxPro.
# '+' is a dBase 7 "Autoincrement" field, stored the same as a long (see http://www.dbase.com/KnowledgeBase/int/db7_file_fmt.htm)
if ($big_endian) {
$recfmt .= "V"; # probably unsigned, unfortunately
} else {
$recfmt .= "l"; # signed
}
} elsif (('M' eq $fldType) && (4 == $fldLen)) {
# Another special case:
# 'M' is a Memo field -- the data is in another file, this is a block
# number referencing the other file. Some databases use a 10-byte
# string to store the block number, others use 4 binary bytes. So
# if the length is 4, we decode it as unsigned binary.
if ($big_endian) {
$recfmt .= "V"; # probably unsigned
} else {
$recfmt .= "L"; # definitely unsigned
}
} elsif (('B' eq $fldType) && (8 == $fldLen)) {
# 8-byte Foxbase/Foxpro double-precision binary (IEEE 64-bit floating pt).
# Stored 52-bit mantissa LSB first, then 11-bit exponent & 1-bit sign.
# Thanks to Roland Baranyai for the sample data!
if ($perl_supports_IEEE754_doubles) {
$recfmt .= "d";
} else {
$recfmt .= "a8";
if (!$warned_about_B_field) {
print "Warning: Foxpro 'B' (double-precision) fields won't be converted correctly\n"
. "because this Perl doesn't support FoxPro's little-endian IEEE-754 64-bit\n"
. "data format. For help, contact Dave Burton at http:\\\\www.burtonsys.com\\email\\\n";
$warned_about_B_field = 1;
# It is quite possible that on big-endian computers this fails
# but could be made to work by simply reversing the byte order.
# But I don't have the ability to test it.
}
}
} elsif (('T' eq $fldType) && (8 == $fldLen)) {
# 8-byte Foxpro date/time field
$recfmt .= "a8"; # Note: a8 preserves zero bytes, A8 truncates them, apparently
} else {
# Normal fields:
$recfmt .= "A$fldLen";
}
$running_offset += $fldLen;
} #for
# The $recfmt unpacking template is complete
# This is a hack for Visual Foxpro. For some reason, Visual Foxpro
# often (always?) puts 8 junk field definition headers after the real
# ones. All 8 always have zero length. The last 7 always have null names,
# types & offsets, too; the first of the eight sometimes has a null name,
# type & offset, but sometimes has the name "\rtastrade.d" with type='b'
# offset=99 (where 99 is character code 'c'). That seems to have something
# to do with a Microsoft test file called "tastrade.dbc". Anyhow, when
# we encounter this, we just ignore the 8 bogus field definitions:
if ($numfields > 8) {
if ( (0==$fld_len[$numfields]) && (0==$fld_len[$numfields-1])
&& (0==$fld_len[$numfields-2]) && (0==$fld_len[$numfields-3])
&& (0==$fld_len[$numfields-4]) && (0==$fld_len[$numfields-5])
&& (0==$fld_len[$numfields-6]) && (0==$fld_len[$numfields-7])
&& (0 != $fld_len[$numfields-8])) {
if ( ('' eq $fld_nam[$numfields]) && ('' eq $fld_nam[$numfields-1])
&& ('' eq $fld_nam[$numfields-2]) && ('' eq $fld_nam[$numfields-3])
&& ('' eq $fld_nam[$numfields-4]) && ('' eq $fld_nam[$numfields-5])
&& ('' eq $fld_nam[$numfields-6]) && ('' ne $fld_nam[$numfields-8])) {
print "Visual FoxPro idiosyncracy: 8 bogus 0-length fields (ignored)\n";
$numfields -= 8;
splice( @fld_nam, $numfields+1, 8 ); # discard last 8 array elements
splice( @fld_ofs, $numfields+1, 8 );
splice( @fld_len, $numfields+1, 8 );
splice( @fld_typ, $numfields+1, 8 );
splice( @fld_flg, $numfields+1, 8 );
$recfmt = substr( $recfmt, 0, length($recfmt)-16 ); # remove the "A0A0A0A0A0A0A0A0" from the end
}
}
}
if ($debugmode) {
printf "recfmt='%s'\n", $recfmt;
}
# Classify each field as supported or unsupported
for ($i=1; $i <= $numfields; $i++) {
$fldType = $fld_typ[$i];
$fld_supported[$i] = 0;
if (index("CDLNIMFBT",$fldType) >= 0) {
$fld_supported[$i] = 1;
if ((8 != $fld_len[$i]) && (('B' eq $fldType) || ('T' eq $fldType))) {
# 'B' and 'T' are only supported if they are 8 bytes long
$fld_supported[$i] = 0;
}
}
if ('M' eq $fldType) {
$has_memo_fields = 1; # optimization
}
} #for
# Lukasz Matusiak (lukaszzp at gmail.com) provided me 21-May-2012 with the
# sample data I needed to support FoxPro's 'T' filed (DateTime). So I
# changed "CDLNIMFB" to "CDLNIMFBT" above. Thanks, Lukasz!
# "Dan" (mail_lodge at yahoo.com.au) emailed me 19-Jul-2005 to say
# that type 'F' fields work fine. They are just text representations of
# floating point numbers, such as "1.97056329250e+000" (that example is
# from Dan's email, and it has a field length = 19). According to
# http://www.dbase.com/KnowledgeBase/int/db7_file_fmt.htm
# type 'F' fields contain "Number stored as a string, right justified, and
# padded with blanks to the width of the field" in dBase 7, which is
# consistent with Dan's report. So I changed "CDLNIM" to "CDLNIMF" above.
# type 'B' is IEEE-754 double-precision (8-byte) floating point, in FoxPro.
# Thanks to Roland Baranyai for the sample data from which I determined this!
# If this Perl supports that data type via the 'd' unpack template, then
# we handle this well. Otherwise, we just represent it as hexidecimal
# in the output file (and display a warning).
# Jacky Bruno comments...
# Definition of output format
# Here will be used the field separator
# (defined by $separe variable modifiable at the beginning of the file)
# You can change quotes used at the left and the right of fields too
# using another character (is it really useful?) by changing " in the
# next variable to the wished character (you can even set a beginning
# character and a ending character) example :
# $csvoutfmt = 'Y%sZ' . ("${separe}Y%sZ" x ($numfields-1)) . "\n";
# field names will have Y before and Z after : Yname_of_fieldZ
# Attention to escape special characters if used
# The () tells that ${separe}"%s" will be repeated ($numfields-1) times
# If there were no questionable data fields, this would suffice:
# $csvoutfmt = '"%s"' . ("${separe}\"%s\"" x ($numfields-1)) . "\n";
# This does the same thing, except that it adds '?' to questionable data fields:
$csvoutfmt = '';
for ($i=1; $i <= $numfields; $i++) {
if ($i > 1) {
$csvoutfmt .= $separe;
}
if ($fld_supported[$i]) {
$csvoutfmt .= '"%s"';
} else {
$csvoutfmt .= '"%s?"';
}
}
$csvoutfmt .= "\n";
# note: we started counting with $i=1 instead of 0 because the DelFlg field
# won't be output
if ($jsonmode) {
$outfmt = '{';
for ($i=1; $i <= $numfields; $i++) {
if ($i > 1) {
$outfmt .= ", ";
}
$outfmt .= ('"' . $fld_nam[$i] . '":"%s"');
}
$outfmt .= "}";
} else {
$outfmt = $csvoutfmt;
}
if ($running_offset != $recLen) {
print "Warning: Summed field lengths (+1 byte for DEL flag) = $running_offset, which is unequal to recLen.\n";
$reclens_look_right = 0;
} else {
print "summed field lengths + 1 = $running_offset = recLen (as expected).\n";
$reclens_look_right = 1;
}
### Begin code to fix field offsets for .dbf files in which the field
### offsets are incorrect or missing altogether
# Are two or more fields at the same field offset? If so then the .dbf
# file definitely doesn't have correct field offsets in the header.
$prev_fldOffset = $fld_ofs[1];
$cnt_idential_fldOffsets = 0;
for ($i=2; $i <= $numfields; $i++) {
$fldOffset = $fld_ofs[$i];
if ($fldOffset == $prev_fldOffset) {
$cnt_idential_fldOffsets++;
}
$prev_fldOffset = $fldOffset;
} #for
# Tell the user about the identical field offsets
if ($cnt_idential_fldOffsets > 0) {
$cnt_idential_fldOffsets++;
print "Warning: ";
if ($cnt_idential_fldOffsets == $numfields) {
print "All "; # say "All nn fields have identical offsets."
}
print "$cnt_idential_fldOffsets fields have identical offsets.\n";
$silent = 0;
if ($cnt_idential_fldOffsets == $numfields) {
print "Note: $infile is in a non-standard .dbf format (such as Alpha-4's),\n" .
"in which the field offsets are missing from the header.\n" .
"The offsets will be recalculated from the summed field lengths.\n";
# Mark Godhelf reported that for his Alpha-4's .dbf files $fldOffset is always zero. 12/13/2002
# Stephane Boireau had a .dbf file in which all the $fldOffsets were 383. 4/13/2003
$silent = 1;
}
}
# Check whether or not the field offsets are consistent with the field lengths,
# and if they are not then tell the user about the problem (unless we already
# told him that all the field offsets are identical).
$running_offset = 1;
$offsets_match_fieldlens = 1;
for ($i=1; $i <= $numfields; $i++) {
$fldLen = $fld_len[$i];
$fldOffset = $fld_ofs[$i];