I wrote a program in perl that manipulates (create, delete, open, close, read, write, copy, etc.) files and directories. It does this very well when running on Linux (Ubuntu) and also on macOS. But it has to do the same job under windows too, and there I have problems with the encodings of file names that contain characters other than ASCII (for example German Umlauts, but also any other non-ASCII characters).
Since my original program is way too big, I created a shorter program for testing.
This is the shortened equivalent of my first, naive version of my perl program (the program file itself is encoded as UTF-8):
#!/usr/bin/perl -w
use strict;
use warnings;
my $filename = 'FäöüßÄÖÜẞçàéâœ.txt';
my $text = 'TäöüßÄÖÜẞçàéâœ';
my $dirname = 'DäöüßÄÖÜẞçàéâœ';
# list all files in the parent directory before any action -------------
listDirectory('.');
# create file and write into file --------------------------------------
print "Going to open file $filename for writing ... ";
if (open(my $fileHandle, '>', $filename)) {
print "done successfully\n";
print "Going to write text '$text' into file $filename ... ";
if (print $fileHandle $text."\n") {
print "done successfully\n";
} else {
errorExit("failed to write into file", __LINE__);
}
close($fileHandle);
} else {
errorExit("failed to open file for writing", __LINE__);
}
# create a new directory -----------------------------------------------
print "Going to create directory $dirname ... ";
if (mkdir($dirname)) {
print "done successfully\n";
} else {
errorExit("failed to create directory", __LINE__);
}
# list all files in the parent directory again -------------------------
listDirectory('.');
# read file ------------------------------------------------------------
print "Going to open file $filename for reading ... ";
if (open(my $fileHandle, '<', $filename)) {
print "done successfully\n";
print "Going to list content of file $filename:\n";
print "--- begin of content ---\n";
while (my $row = <$fileHandle>) {
chomp $row;
print "$row\n";
}
print "--- end of content ---\n\n";
close($fileHandle);
} else {
errorExit("failed to open file for reading", __LINE__);
}
# list all files in the newly created directory ------------------------
listDirectory($dirname);
# end ------------------------------------------------------------------
print "normal end of execution\n";
exit(0);
# subroutines ==========================================================
# list all files in a directory ----------------------------------------
sub listDirectory {
my $dir = shift;
my $dirname = $dir eq '.' ? 'parent directory' : $dir;
print "Content of $dirname\n";
if (opendir (my $dirHandle, $dir)) {
print "--- begin of content of $dirname ---\n";
while (my $file = readdir($dirHandle)) {
print "$file\n";
}
print "--- end of content of $dirname ---\n\n";
closedir($dirHandle);
} else {
errorExit("failed to open $dirname", __LINE__);
}
}
# Error exit -----------------------------------------------------------
sub errorExit {
my $message = shift;
my $line = shift;
print "Error before line $line:\n";
print "program message: $message\n";
print "system message: $!\n";
print "premature end of execution\n";
exit(0);
}
Output of my program in macOS and in Linux (Ubuntu):
Content of parent directory
--- begin of content of parent directory ---
.
..
testUmlaut.pl
--- end of content of parent directory ---
Going to open file FäöüßÄÖÜẞçàéâœ.txt for writing ... done successfully
Going to write text 'TäöüßÄÖÜẞçàéâœ' into file FäöüßÄÖÜẞçàéâœ.txt ... done successfully
Going to create directory DäöüßÄÖÜẞçàé✠... done successfully
Content of parent directory
--- begin of content of parent directory ---
.
..
testUmlaut.pl
FäöüßÄÖÜẞçàéâœ.txt
DäöüßÄÖÜẞçàéâœ
--- end of content of parent directory ---
Going to open file FäöüßÄÖÜẞçàéâœ.txt for reading ... done successfully
Going to list content of file FäöüßÄÖÜẞçàéâœ.txt:
--- begin of content ---
TäöüßÄÖÜẞçàéâœ
--- end of content ---
Content of DäöüßÄÖÜẞçàéâœ
--- begin of content of DäöüßÄÖÜẞçàé✠---
.
..
--- end of content of DäöüßÄÖÜẞçàé✠---
normal end of execution
This is the expected output.
But I get this when I execute this program on a windows machine:
Content of parent directory
--- begin of content of parent directory ---
.
..
testUmlaut.pl
--- end of content of parent directory ---
Going to open file F├ñ├Â├╝├ƒ├ä├û├£ß║×├º├á├®├ó┼ô.txt for writing ... done successfully
Going to write text 'T├ñ├Â├╝├ƒ├ä├û├£ß║×├º├á├®├ó┼ô' into file F├ñ├Â├╝├ƒ├ä├û├£ß║×├º├á├®├ó┼ô.txt ... done successfully
Going to create directory D├ñ├Â├╝├ƒ├ä├û├£ß║×├º├á├®├ó┼ô ... done successfully
Content of parent directory
--- begin of content of parent directory ---
.
..
testUmlaut.pl
F├ñ├Â├╝├ƒ├ä├û├£ß║×├º├á├®├ó┼ô.txt
D├ñ├Â├╝├ƒ├ä├û├£ß║×├º├á├®├ó┼ô
--- end of content of parent directory ---
Going to open file F├ñ├Â├╝├ƒ├ä├û├£ß║×├º├á├®├ó┼ô.txt for reading ... done successfully
Going to list content of file F├ñ├Â├╝├ƒ├ä├û├£ß║×├º├á├®├ó┼ô.txt:
--- begin of content ---
T├ñ├Â├╝├ƒ├ä├û├£ß║×├º├á├®├ó┼ô
--- end of content ---
Content of D├ñ├Â├╝├ƒ├ä├û├£ß║×├º├á├®├ó┼ô
--- begin of content of D├ñ├Â├╝├ƒ├ä├û├£ß║×├º├á├®├ó┼ô ---
.
..
--- end of content of D├ñ├Â├╝├ƒ├ä├û├£ß║×├º├á├®├ó┼ô ---
normal end of execution
So, all filenames are written with a wrong encoding. Also in the explorer you see misscoded filenames for the new file and directory. But although the text file contains the correct content, my program displays it wrong.
So, I fiddeled around with my program until I got a version, that produces the correct output (identical to the output of the first naïve version under macOS and Linux)).
But in the file system the filenames are still wrong:
13.01.2020 17:36 <DIR> .
10.01.2020 14:46 <DIR> ..
13.01.2020 18:23 2 970 testUmlaut.pl
13.01.2020 18:23 30 FäöüßÄÖÜẞçà éâœ.txt
13.01.2020 18:23 <DIR> DäöüßÄÖÜẞçà éâœ
Here is the code of the new version of my program:
#!/usr/bin/perl -w
use strict;
use warnings;
use utf8;
use Encode;
if ($^O eq 'MSWin32') {
require Win32::Console;
Win32::Console::OutputCP(65001);
}
binmode STDOUT, ":utf8";
my $filename = 'FäöüßÄÖÜẞçàéâœ.txt';
my $text = 'TäöüßÄÖÜẞçàéâœ';
my $dirname = 'DäöüßÄÖÜẞçàéâœ';
# list all files in the parent directory before any action -------------
listDirectory('.');
# create file and write into file --------------------------------------
print "Going to open file $filename for writing ... ";
if (open(my $fileHandle, '>:encoding(UTF-8)', $filename)) {
print "done successfully\n";
print "Going to write text '$text' into file $filename ... ";
if (print $fileHandle $text."\n") {
print "done successfully\n";
} else {
errorExit("failed to write into file", __LINE__);
}
close($fileHandle);
} else {
errorExit("failed to open file for writing", __LINE__);
}
# create a new directory -----------------------------------------------
print "Going to create directory $dirname ... ";
if (mkdir($dirname)) {
print "done successfully\n";
} else {
errorExit("failed to create directory", __LINE__);
}
# list all files in the parent directory again -------------------------
listDirectory('.');
# read file ------------------------------------------------------------
print "Going to open file $filename for reading ... ";
if (open(my $fileHandle, '<:encoding(UTF-8)', $filename)) {
print "done successfully\n";
print "Going to list content of file $filename:\n";
print "--- begin of content ---\n";
while (my $row = <$fileHandle>) {
chomp $row;
print "$row\n";
}
print "--- end of content ---\n\n";
close($fileHandle);
} else {
errorExit("failed to open file for reading", __LINE__);
}
# list all files in the newly created directory ------------------------
listDirectory($dirname);
# end ------------------------------------------------------------------
print "normal end of execution\n";
exit(0);
# subroutines ==========================================================
# list all files in a directory ----------------------------------------
sub listDirectory {
my $dir = shift;
my $dirname = $dir eq '.' ? 'parent directory' : $dir;
print "Content of $dirname\n";
if (opendir (my $dirHandle, $dir)) {
print "--- begin of content of $dirname ---\n";
while (my $file = decode_utf8(readdir($dirHandle))) {
print "$file\n";
}
print "--- end of content of $dirname ---\n\n";
closedir($dirHandle);
} else {
errorExit("failed to open $dirname", __LINE__);
}
}
# Error exit -----------------------------------------------------------
sub errorExit {
my $message = shift;
my $line = shift;
print "Error before line $line:\n";
print "program message: $message\n";
print "system message: $!\n";
print "premature end of execution\n";
exit(0);
}
This new version still behaves well when running in Linux or macOS. But there is still this issue with the filenames in Windows.
How can I fix this?
Windows system calls that accept/return string come in two varieties. The "A" (ANSI) version that deals with text encoded using the system's Active Code Page, and the "W" (Wide) version that deals with text encoded using UTF-16le.
Perl uses the "A" version exclusively, and thus expects file names to be encoded using the Active Code Page (e.g. cp1252 for most US machines.)
One solution is to encode the file name using the correct code page.
use utf8; # Source code encoded using UTF-8.
my ($cie, $coe, $ae);
BEGIN {
require Win32;
$cie = "cp" . Win32::GetConsoleCP();
$coe = "cp" . Win32::GetConsoleOutputCP();
$ae = "cp" . Win32::GetACP();
binmode(STDIN, ":encoding($cie)");
binmode(STDOUT, ":encoding($coe)");
binmode(STDERR, ":encoding($coe)");
require "open.pm";
"open"->import(":encoding($ae)"); # Default encoding for open()
}
use Encode qw( encode );
#my $qfn = 'FäöüßÄÖÜẞçàéâœ.txt';
my $qfn = 'FäöüßÄÖÜßçàéâœ.txt';
open(my $fh, '>', encode($ae, $qfn))
or die("Can't create \"$qfn\": $!\n");
print($fh "This is \"$qfn\".\n");
Note that I replaced "ẞ" with "ß" because "ẞ" isn't present in the character set of my active code page (cp1252), and thus couldn't be used as part of the file name. To avoid this problem, one needs to use the Wide interface. This can be achieved using Win32::Unicode::File and Win32::Unicode::Dir, or Win32::LongPath.
use utf8; # Source code encoded using UTF-8.
my ($cie, $coe, $ae);
BEGIN {
require Win32;
$cie = "cp" . Win32::GetConsoleCP();
$coe = "cp" . Win32::GetConsoleOutputCP();
$ae = "cp" . Win32::GetACP();
binmode(STDIN, ":encoding($cie)");
binmode(STDOUT, ":encoding($coe)");
binmode(STDERR, ":encoding($coe)");
require "open.pm";
"open"->import(":encoding($ae)"); # Default encoding for open()
}
use Win32::Unicode::File qw( );
my $qfn = 'FäöüßÄÖÜẞçàéâœ.txt';
my $fh = Win32::Unicode::File->new('>', $qfn)
or die("Can't create \"$qfn\": $!\n");
binmode($fh, ":encoding($ae)"); # Didn't happen automatically since we didn't use open()
print($fh "This is \"$qfn\".\n");
Read a bit more about this here.
Related
this is my Perl script
use strict;
use warnings;
use XML::Twig;
use Data::Dumper;
sub xml2array{
my $path = shift;
my $twig = XML::Twig->new->parsefile($path);
return map { $_ -> att('VirtualPath') } $twig -> get_xpath('//Signals');
}
sub compareMappingToArray {
my $mapping = shift;
my $signalsRef = shift;
my $i = 1;
print "In file : $mapping\n";
open(my $fh, $mapping);
while (my $r = <$fh>) {
chomp $r;
if ($r =~ /\'(ModelSpecific.*)\'/) {
my $s = $1;
my #matches = grep { /^$s$/ } #{$signalsRef};
print "line $i : not found - $s\n" if scalar #matches ==0;
print "line $i : multiple $s\n" if scalar #matches > 1;
}
$i = $i + 1 # keep line index
}
}
my $mapping = "C:/Users/HOR1DY/Desktop/Global/TA_Mapping/CAN/CAN_ESP_002_mapping.pm";
my #virtualpath = xml2array("SignalModel.xml");
compareMappingToArray($mapping, \#virtualpath);
The script works well, the aim of it is to compare the file "SignalModel.xml" and "CAN_ESP_002_mapping.pm" and putting the lines that didn't matches in a .TXT file. Here is how the .TXT file looks like:
In file : C:/Users/HOR1DY/Desktop/Global/TA_Mapping/CAN/CAN_ESP_002_mapping.pm
line 331 : not found - ModelSpecific.EID.NET.CAN_Engine.VCU.Transmit.VCU_202.R2B_VCU_202__byte_3
line 348 : not found - ModelSpecific.EID.NET.CAN_Engine.CMM_WX.Transmit.CMM_HYB_208.R2B_CMM_HYB_208__byte_2
line 368 : not found - ModelSpecific.EID.NET.CAN_Engine.VCU.Transmit.VCU_222.R2B_VCU_222__byte_0
But for this script, I put the two files that need to be compare inside of the code and instead of doing that, I would like to run the script in windows cmd line and having something like:
C:\Users>perl CANMappingChecker.pl -'file 1' 'file 2'
All the files are in .zip file so if I can execute the script that he goes inside and take the 2 files that I need for comparison, it should be perfect.
I really don't know how to do and what to put inside my script to make that in the cmd windows. Thanks for your help !
Program (or script) parameters are stored in the #ARGV array. shift and pop without any parameter will work on #ARGV when used outside of a sub, in a sub they operate on #_.
See Archive::Zip for zip file handling.
Using Strawberry Perl 5.22.0 in Windows 7. Is there a more "perlish" way to write this snippet of code? I hate the duplication of file open sections, but cannot think of a way to make it only open once because of the requirement to test the creation time.
...
my $x;
my $fh;
my $sentinelfile = "Logging.yes"; #if this file exists then enable logging
my $logfile = "transfers.log";
my $log = 0; #default to NO logging
$log = 1 if -e $sentinelfile; #enable logging if sentinel file exists
if($log){
#logfile remains open after this so remember to close at end of program!
if (-e $logfile) { #file exists
open($fh, "<", $logfile); #open for read will NOT create if not exist
chomp ($x = <$fh>); #grab first row
close $fh;
if (((scalar time - $x)/3600/24) > 30) { #when ~30 days since created
rename($logfile, $logfile . time); #rename existing logfile
open($fh, ">", $logfile); #open for write and truncate
print $fh time,"\n"; #save create date
print $fh "--------------------------------------------------\n";
} else { #file is not older than 30 days
open($fh, ">>", $logfile); #open for append
}
} else { #file not exist
open($fh, ">", $logfile); #open new for write
print $fh time,"\n"; #save create date
print $fh "--------------------------------------------------\n";
}
} #if $log
...
To recap: logfile logs stuff. First row of file contains the logfile creation date. Second row contains horizontal rule. Rest of file contains text. Around 30 days after file was created, rename file and start a new one. After the above chunk of code the logfile is open and ready for logging stuff. It gets closed at the end of the rest of the program.
There are other, non-cosmetic problems with your code: a) You do not ever check if your calls to open succeeded; b) You are creating a race condition. The file can come into existence after the -e check has failed. The subsequent open $fh, '>' ... would then clobber it; c) You don't check if your rename call succeeded etc.
The following would be a partial improvement on your existing code:
if ($log) {
if (open $fh, '<', $logfile) { #file exists
chomp ($x = <$fh>);
close $fh
or die "Failed to close '$logfile': $!";
if (((time - $x)/3600/24) > 30) {
my $rotated_logfile = join '.', $logfile, time;
rename $logfile => $rotated_logfile
or die "Failed to rename '$logfile' to '$rotated_logfile': $!";
open $fh, '>', $logfile
or die "Failed to create '$logfile'";
print $fh time, "\n", '-' x 50, "\n";
}
else {
open $fh, '>>', $logfile
or die "Cannot open '$logfile' for appending: $!";
}
}
else {
open $fh, '>', $logfile
or die "Cannot to create '$logfile': $!";
print $fh time, "\n", '-' x 50, "\n";
}
}
It would be better to abstract every bit of discrete functionality to suitably named functions.
For example, here is a completely untested re-write:
use autouse Carp => qw( croak );
use constant SENTINEL_FILE => 'Logging.yes';
use constant ENABLE_LOG => -e SENTINEL_FILE;
use constant HEADER_SEPARATOR => '-' x 50;
use constant SECONDS_PER_DAY => 24 * 60 * 60;
use constant ROTATE_AFTER => 30 * SECONDS_PER_DAY;
my $fh;
if (ENABLE_LOG) {
if (my $age = read_age( $logfile )) {
if ( is_time_to_rotate( $age ) ) {
rotate_log( $logfile );
}
else {
$fh = open_log( $logfile );
}
}
unless ($fh) {
$fh = create_log( $logfile );
}
}
sub is_time_to_rotate {
my $age = shift;
return $age > ROTATE_AFTER;
}
sub rotate_log {
my $file = shift;
my $saved_file = join '.', $file, time;
rename $file => $saved_file
or croak "Failed to rename '$file' to '$saved_file': $!"
return;
}
sub create_log {
my $file = shift;
open my $fh, '>', $file
or croak "Failed to create '$file': $!";
print $fh time, "\n", HEADER_SEPARATOR, "\n"
or croak "Failed to write header to '$file': $!";
return $fh;
}
sub open_log {
my $file = shift;
open my $fh, '>>', $file
or croak "Failed to open '$file': $!";
return $fh;
}
sub read_age {
my $file = shift;
open my $fh, '<', $file
or return;
defined (my $creation_time = <$fh>)
or croak "Failed to read creation time from '$file': $!";
return time - $creation_time;
}
If you need to read a line of a file, rename it and then work with it, you have to open it twice.
However, you can also do away with using that first line.
On Windows, according to perlport (Files and Filesystems), the inode change time time-stamp (ctime) "may really" mark the file creation time. This is likely to be completely suitable for a log file that doesn't get manipulated and moved around. It can be obtained with the -C file-test operator
my $days_float = -C $filename;
Now you can numerically test this against 30. Then there is no need to print the file's creation time to its first line (but you may as well if it is useful for viewing or other tools).
Also, there is the module Win32API::File::Time, with the purpose to
provide maximal access to the file creation, modification, and access times under MSWin32
Plese do read the docs for some caveats. I haven't used it but it seems tailored for your need.
A good point is raised in a comment: apparently the OS retains the original time-stamp as the file is being renamed. In that case, when the file's too old copy it into a new one (with the new name) and delete it, instead of using rename. Then open that log file anew, so with a new time-stamp.
Here is a complete example
archive_log($logfile) if -f $logfile and -C $logfile > 30;
open my $fh_log, '>>', $logfile or die "Can't open $logfile: $!";
say $fh_log "Log a line";
sub archive_log {
my ($file) = #_;
require POSIX; POSIX->import('strftime');
my $ts = strftime("%Y%m%d_%H:%M:%S", localtime); # 20170629_12:44:10
require File::Copy; File::Copy->import('copy');
my $archive = $file . "_$ts";
copy ($file, $archive) or die "Can't copy $file to $archive: $!";
unlink $file or die "Can't unlink $file: $!";
}
The archive_log archives the current log by copying it and then removes it.
So after that we can just open for append, which creates the file if not there.
The -C tests for file existence but since its output is used in a numerical test we need -f first.
Since this happens once a month I load modules at runtime, with require and import, once the log actually need be rotated. If you already use File::Copy then there is no need for this. As for the time-stamp, I threw in something to make this a working example.
I tested this on UNIX, by changing -C to -M and tweaking the timestamp by touch -t -c.
Better yet, to reduce the caller's code fully move the tests into the sub as well, for
my $fh_log = open_log($logfile);
say $fh_log "Log a line";
sub open_log {
my ($file) = #_;
if (-f $file and -C $file > 30) {
# code from archive_log() above, to copy and unlink $file
}
open my $fh_log, '>>', $file or die "Can't open $file: $!";
return $fh_log;
}
Note. On UNIX the file's creation time is not kept anywhere. The closest notion is the ctime above, but this is of course different. For one thing, it changes with many operations, for instance mv, ln, chmod, chown, chgrp (and probably others).
I am redirecting STDERR to an error file but am unable to unlink the error file if it is empty. I believe that I am not releasing STDERR which is rendering the error file busy and cannot be deleted. What do you think? Thank you!
$errFile = $outFile . "-error";
open (ERRFILE, '>', $errFile) or die $!;
#Redirect STDERR from the console to the error log
open (STDERR, '>', $errFile) or die $!;
# Do stuff....
close(STDERR);
close(ERRFILE);
#Remove blank error files
opendir(DIR, 'c:\LMITS');
#errFiles = grep /error/, readdir DIR;
closedir DIR;
foreach $errFile (#errFiles) {
$errFileSize = -s $errFile;
if ($errFileSize == 0) {
unlink $errFile;
}
}
readdir returns file names, not paths.
foreach (#errFiles) {
my $errFile = 'c:\\LMITS\\' . $_;
...
}
This code works but if I move the commands to close SDTERR and ERRFILE down in the script any a blank ERRFILE will not be deleted. I'm ok for now but I will keep researching for the sake of just knowing.
use CQPerlExt;
use POSIX qw(strftime);
use Strict;
my $checkForBlanks;
my $dbConfig;
my $dbConfigRecord;
my $entitydef;
my $errFile;
my #errFiles;
my $errFileSize;
my $fileDate;
my #fieldNames;
my $fieldName;
my $lastSync;
my $outFile;
my $queryDef;
my $queryResults;
my $recordCount = 0;
my $recordType;
my $session;
my $scriptStartTime;
my $swCR;
my $swData;
my $swID;
# ##############################################
# ##### Process administrative items and
# ##### establish a ClearQuest session
# ##############################################
$scriptStartTime = strftime("%Y-%m-%d %I:%M:%S %p", localtime());
$fileDate = strftime("%Y%m%d_%I%M%S%p", localtime());
#Create and open the output and error files
$outFile = "MSTU_Unclass_Export"."_".$fileDate.".txt";
open (OUTFILE, ">", $outFile) or die $!;
$errFile = $outFile . "-error";
open (ERRFILE, '>', $errFile) or die $!;
#Redirect STDERR from the console to the error log
open (STDERR, '>', $errFile) or die $!;
$session = CQSession::Build();
CQSession::UserLogon($session, "uname", "pw", "db", "schema");
$dbConfigRecord = $session->GetEntity("DB_CONFIG", "33554467");
$lastSync = $dbConfigRecord->GetFieldStringValue("LastSyncDate");
# ##############################################
# ##### Query the database for all SWCRs
# ##### updated after lastSyncDate
# ##############################################
$queryDef = $session->BuildQuery("SWCR");
$queryDef->BuildField("dbid");
#lastSyncDate = ($lastSync);
$operator = $queryDef->BuildFilterOperator($CQPerlExt::CQ_BOOL_OP_AND);
$operator->BuildFilter ("history.action_timestamp", $CQPerlExt::CQ_COMP_OP_GTE,\#lastSyncDate);
$queryResults = $session->BuildResultSet($queryDef);
$queryResults->Execute();
# ##############################################
# ##### Build a text file with SWCR data associated
# ##### with the dbids returned above
# ##############################################
#Get all of the fieldnames you want to export
$recordType = 'SWCR';
$entitydef = $session->GetEntityDef($recordType);
#fieldNames = #{$entitydef->GetFieldDefNames()};
#Remove any fields you don't want
#fieldNames = grep ! /dbid|history|RecordID|CCObjects|MergeSWCRs|AssociatedIntegrationSet|Level1TestResults|
Level2TestResults|Level3TestResults|Level4TestResults|Reviews|WithdrawCR|
AssociatedWithdrawnCR|Attachments|AssociatedPRs|OriginatingSolution|AssociatedSWRsFull|
AssociatedSWRsDelta|ClonedFrom|ClonedTo|AssociatedComment|ExternalLinks|ratl_mastership/x, #fieldNames;
while ($queryResults->MoveNext() == $CQPerlExt::CQ_SUCCESS) {
$swCR = $session->GetEntityByDbId("SWCR", $queryResults->GetColumnValue(1));
#Gather data
$swID = $swCR->GetFieldValue("RecordID")->GetValue();
$swData = "<RecordID>" . $swID . "</RecordID>";
foreach $fieldName (#fieldNames)
{
$checkForBlanks = $swCR->GetFieldStringValue($fieldName);
if ($checkForBlanks ne ""){
$swData = $swData . "<" . $fieldName . ">" . $swCR->GetFieldStringValue($fieldName) . "</" . $fieldName . ">";
}
}
#Build file with records seperated by custom line delimiter
print OUTFILE $swData . "~~lineDelimiter~~\n";
#Keep track of the amount of records being exported
$recordCount++;
}
close(STDERR);
close(ERRFILE);
close(OUTFILE);
# ##############################################
# ##### Process administrative items and
# ##### close ClearQuest session
# ##############################################
#Remove extra carriage return at bottom of export file because this will throw an error when an import is performed
truncate($outFile, (-s $outFile) - 2);
#Add amount of records exported to the export log
open (EXPLOG, ">>", 'Export_Log.txt') or die $!;
print EXPLOG "$scriptStartTime: $recordCount record(s) written to $outFile for export.\n";
close (EXPLOG);
#Set the LastSyncDate field to the time the export script started
$dbConfigRecord = $session->GetEntity("DB_CONFIG", "33554467");
$session->EditEntity($dbConfigRecord, "Modify");
$dbConfigRecord->SetFieldValue("LastSyncDate",$scriptStartTime);
$dbConfigRecord->Validate();
$dbConfigRecord->Commit();
#Remove blank error files
opendir(DIR, 'c:\LMITS');
#errFiles = grep /error/, readdir DIR;
closedir DIR;
foreach $errFile (#errFiles) {
$errFileSize = -s $errFile;
if ($errFileSize == 0) {
unlink $errFile;
}
}
CQSession::Unbuild($session);
This is my code:
opendir(DIR, $directoryPath) or die "Cant open $directoryPath$!";
my #files = readdir(DIR); #Array of file names
closedir (DIR) or die "Cant close $directoryPath$!";
I'm using #files to create an array of the file names within the directory for renaming later in the program.
The problem is:
I am getting the error "Bad Symbol for dirhandle" at the closedir line.
If I don't closedir to avoid this, I don't have permission to change file names (I'm using Windows).
I tried an alternative way of renaming the files (below) to try a different solution to the problem by renaming the files a different way and within the dirhandles, but this just repeat the permission errors.
opendir(DIR, $directoryPath) or die "Cant open $directoryPath$!";
while( (my $filename = readdir(DIR)))
{
rename($filename, $nFileName . $i) or die "Cant rename file $filename$!";
i++;
}
closedir (DIR) or die "Cant close $directoryPath$!";
From a quick bit of research I think the permission error is a Windows security feature so you can't edit a file while its open, but I haven't been able to find a solution simple enough for me to understand.
An answer to point 1. or point 3. is preferrable, but an answer to point 2. will also be useful.
Full code used in points 1. and 2. below
use 5.16.3;
use strict;
print "Enter Directory: ";
my $directoryPath = <>;
chomp($directoryPath);
chdir("$directoryPath") or die "Cant chdir to $directoryPath$!";
opendir(DIR, $directoryPath) or die "Cant open $directoryPath$!";
my #files = readdir(DIR); #Array of file names
closedir (DIR) or die "Cant close $directoryPath$!";
my $fileName = "File ";
for my $i (0 .. #files)
{
rename($files[$i], $fileName . ($i+1)) or die "Cant rename file $files[$i]$!";
}
chdir; #return to home directory
I can input the path correctly, but then error message (copied exactly) is:
Can't rename file .Permission denied at C:\path\to\file\RenameFiles.pl line 19, <> line 1.
The error
Can't rename file .Permission denied at C:\path\to\file\RenameFiles.pl line 19, <> line 1.
says that you are trying to rename the file ., which is a special file that is a shortcut for "current directory". You should add exceptions to your code to not rename this file, and the one called ... Something like:
next if $files[$i] =~ /^\./;
Would do. This will skip over any file that begins with a period .. Alternatively you can skip directories:
next if -d $files[$i]; # skip directories (includes . and ..)
As TLP has already pointed out, readdir returns . and .. which corresponds to the current and parent directory.
You'll need to filter those out in order to avoid renaming directories.
use strict;
use warnings;
use autodie;
print "Enter Directory: ";
chomp( my $dirpath = <> );
opendir my $dh, $dirpath or die "Can't open $dirpath: $!";
my $number = 0;
while ( my $file = readdir($dh) ) {
next if $file =~ /^\.+$/;
my $newfile = "$dirpath/File " . ++$number;
rename "$dirpath/$file", $newfile or die "Cant rename file $file -> $newfile: $!";
}
closedir $dh;
Cross Platform Compatibility using Path::Class
One way to simplify this script and logic is to use Path::Class to handle file and directory operations.
use strict;
use warnings;
use autodie;
use Path::Class;
print "Enter Directory: ";
chomp( my $dirname = <> );
my $dir = dir($dirname);
my $number = 0;
for my $file ( $dir->children ) {
next if $file->is_dir();
my $newfile = $dir->file( "File" . ++$number );
$file->move_to($newfile);
}
I'm trying to emulate RapidCRC's ability to check crc32 values within filenames on Windows Vista Ultimate 64-bit. However, I seem to be running into some kind of argument limitation.
I wrote a quick Perl script, created a batch file to call it, then placed a shortcut to the batch file in %APPDATA%\Microsoft\Windows\SendTo
This works great when I select about 20 files or less, right-click and "send to" my batch file script. However, nothing happens at all when I select more than that. I suspect there's a character or number of arguments limit somewhere.
Hopefully I'm missing something simple and that the solution or a workaround isn't too painful.
References:
batch file (crc32_inline.bat):
crc32_inline.pl %*
Perl notes:
I'm using (strawberry) perl v5.10.0
I have C:\strawberry\perl\bin in my path, which is where crc32.bat exists.
perl script (crc32_inline.pl):
#!/usr/bin/env perl
use strict;
use warnings;
use Cwd;
use English qw( -no_match_vars );
use File::Basename;
$OUTPUT_AUTOFLUSH = 1;
my $crc32_cmd = 'crc32.bat';
my $failure_report_basename = 'crc32_failures.txt';
my %failures = ();
print "\n";
foreach my $arg (#ARGV) {
# if the file has a crc, check to see if it matches the calculated
# crc.
if (-f $arg and $arg =~ /\[([0-9a-f]{8})\]/i) {
my $crc = uc $1;
my $basename = basename($arg);
print "checking ${basename}... ";
my $calculated_crc = uc `${crc32_cmd} "${arg}"`;
chomp($calculated_crc);
if ($crc eq $calculated_crc) {
print "passed.\n";
}
else {
print "FAILED (calculated ${calculated_crc})\n";
my $dirname = dirname($arg);
$failures{$dirname}{$basename} = $calculated_crc;
}
}
}
print "\nReport Summary:\n";
if (scalar keys %failures == 0) {
print " All files OK\n";
}
else {
print sprintf(" %d / %d files failed crc32 validation.\n" .
" See %s for details.\n",
scalar keys %failures,
scalar #ARGV,
$failure_report_basename);
my $failure_report_fullname = $failure_report_basename;
if (defined -f $ARGV[0]) {
$failure_report_fullname
= dirname($ARGV[0]) . '/' . $failure_report_basename;
}
$OUTPUT_AUTOFLUSH = 0;
open my $fh, '>' . $failure_report_fullname or die $!;
foreach my $dirname (sort keys %failures) {
print {$fh} $dirname . "\n";
foreach my $basename (sort keys %{$failures{$dirname}}) {
print {$fh} sprintf(" crc32(%s) basename(%s)\n",
$failures{$dirname}{$basename},
$basename);
}
}
close $fh;
$OUTPUT_AUTOFLUSH = 1;
}
print sprintf("\n%s done! (%d seconds elapsed)\n" .
"Press enter to exit.\n",
basename($0),
time() - $BASETIME);
<STDIN>;
I will recommend just putting a shortcut to your script in the "Send To" directory instead of doing it via a batch file (which is subject to cmd.exes limits on command line length).