");
+
+ if ($status{'compileErrs'}) # $validateCases == 0
+ {
+ $status{'feedback'}->print(<Your Specification Tests failed to compile correctly against
+the reference implementation.
+
This is most likely because you have not followed the correct format
+for the specification tests (using only methods defined in the interface).
+
Failure to follow these constraints will prevent the proper assessment
+of your solution and your tests.
+EOF
+ if ($status{'compileMsgs'} ne "")
+ {
+ $status{'feedback'}->print(<The following specific error(s) were discovered while compiling
+your specification tests against the reference implementation:
+
+
\n");
+ }
+ }
+ elsif ($studentsMustSubmitTests
+ && !$status{'studentTestResults'}->hasResults)
+ {
+ $status{'feedback'}->print(<You are required to write your own software tests
+for this assignment. You must provide your own tests
+to get further feedback.
+EOF
+ }
+ elsif ($status{'validateTestResults'}->allTestsFail)
+ {
+ $status{'feedback'}->print(<Your problem setup does not appear to be
+consistent with the assignment.
+EOF
+ if ($studentsMustSubmitTests)
+ {
+ $status{'feedback'}->print(<For this assignment, your test cases are being assessed by running
+your tests against the reference solution.
+EOF
+ }
+ $status{'feedback'}->print(<In this case, none of your specification tests pass on the reference
+solution, which may mean that your specification tests make incorrect
+assumptions about some aspect of the required behavior. This discrepancy prevented
+Web-CAT from properly assessing the thoroughness of your test cases.
+
Double check that you have carefully followed all initial conditions
+requested in the assignment in setting up your test cases.
+EOF
+
+ }
+ elsif ($status{'validateTestResults'}->allTestsPass)
+ {
+ $status{'feedback'}->print(<Your tests appear to match the expectations for this assignment since none expect
+outputs that conflict with the reference implementation.
+EOF
+ }
+ else
+ {
+ if ($studentsMustSubmitTests)
+ {
+ $status{'feedback'}->print(<For this assignment, your test cases are being assessed by running
+your tests against the reference solution.
+
Some of your tests fail when run against the reference implementation.
+
This happens when your test cases embody misconceptions of the problem
+spec by expecting different output from what the reference implementation
+generates for this that test case.
+
Your test cases contain misconceptions of the problem spec, so your
+testing is incomplete.
+EOF
+ }
+ $status{'feedback'}->print(<Double check that you have carefully followed all requirements of the
+assignment when setting up your tests.
+EOF
+ }
+ if ($hintsLimit != 0 && !$status{'compileErrs'})
+ {
+ if ($studentsMustSubmitTests
+ && $hasJUnitErrors
+ && $junitErrorsHideHints)
+ {
+ $status{'feedback'}->print(<Your JUnit test classes contain problems that must be
+fixed before you can receive any more specific feedback. Be sure that
+all of your test classes contain test methods, and that all of your test
+methods include appropriate assertions to check for expected behavior.
+You must fix these problems with your own tests to get further feedback.
+EOF
+ }
+ }
+
+ {
+ if ($codingSectionStatus{'compilerErrors'} == 1)
+ {
+ # Transform the plain text JUnit results into an interactive HTML
+ # view.
+ JavaTddPlugin::transformTestResults('validate_',
+ "$resultDir/validate-results.txt",
+ "$resultDir/validate-results.html"
+ );
+ }
+
+ if ($codingSectionStatus{'compilerErrors'} == 1)
+ {
+ open(VALIDATERESULTS, "$resultDir/validate-results.html");
+ my @lines = ;
+ close(VALIDATERESULTS);
+ if ($#lines >= 0)
+ {
+ $status{'feedback'}->print(<The results of running your test cases are shown
+below. Click on a failed test to see the reason for the failure and an
+execution trace that shows where the error occurred.
+EOF
+ $status{'feedback'}->print(@lines);
+ }
+ unlink "$resultDir/validate-results.html";
+
+ @lines = linesFromFile("$resultDir/validate-out.txt", 75000, 4000);
+ if ($#lines >= 0)
+ {
+ $status{'feedback'}->startFeedbackSection(
+ "Output from your tests", ++$expSectionId, 1, 2,
+ "
", "
");
+ $status{'feedback'}->print(@lines);
+ $status{'feedback'}->endFeedbackSection;
+ }
+ $status{'feedback'}->endFeedbackSection;
+ }
+ }
+ }
+}
+
+
+#=============================================================================
+# generate milestone results
+#=============================================================================
+@milestoneResults = ();
+for (my $i = 0; $i < $milestoneCount; $i++)
+{
+ my $mNum = defined($milestoneNumbers[$i]) ? $milestoneNumbers[$i] : ($i + 1);
+ my $dueDate = $milestoneDueDatesTimestamps[$i];
+ my $reqStudentTests = $milestoneMinStudentTests[$i] // 0;
+ my $reqRefTests = $milestoneMinRefTests[$i] // 0;
+ my $reqCover = $milestoneMinMutationCoverages[$i] // 0;
+ my $reqStyle = $milestoneStyleMins[$i] // 0;
+
+ my $actualStudentTests = 0;
+ my $actualRefTests = 0;
+ my $actualCoverage = 0;
+ my $actualStyle = 0;
+ my @details = ();
+ my $met = 1;
+
+ if (defined $status{'validateTestResults'})
+ {
+ $actualStudentTests = $status{'validateTestResults'}->testsExecuted
+ - $status{'validateTestResults'}->testsFailed;
+ }
+
+ if (defined $status{'instrTestResults'})
+ {
+ $actualRefTests = $status{'instrTestResults'}->testsExecuted
+ - $status{'instrTestResults'}->testsFailed;
+ }
+
+ if (defined $testingSectionStatus{'codeCoveragePercent'})
+ {
+ $actualCoverage = $testingSectionStatus{'codeCoveragePercent'};
+ }
+
+ if (defined $styleSectionStatus{'pointsGainedPercent'})
+ {
+ $actualStyle = defined $styleSectionStatus{'pointsGained'}
+ ? $styleSectionStatus{'pointsGained'}
+ : $styleSectionStatus{'pointsGainedPercent'};
+ }
+
+ my $subTime = $cfg->getProperty('submissionTimestamp', 0);
+ my $dueMet = 1;
+ my $dueExpected = 'Not required';
+ my $dueActual = 'No due date';
+
+ if (defined($dueDate) && $dueDate > 0)
+ {
+ $dueExpected = formatTimestampForDisplay($dueDate);
+ $dueActual = formatTimestampForDisplay($subTime);
+
+ if (defined $subTime && $subTime ne '' && $subTime =~ /^\d+$/
+ && $subTime > $dueDate)
+ {
+ $dueMet = 0;
+ }
+ }
+
+ push @details, {
+ name => 'Due Date',
+ met => $dueMet,
+ expected => $dueExpected,
+ actual => $dueActual
+ };
+ $met = 0 unless $dueMet;
+
+ my $studentMet = 1;
+ my $studentExpected = 'Not required';
+ if ($reqStudentTests > 0)
+ {
+ $studentExpected = ">= $reqStudentTests";
+ $studentMet = ($actualStudentTests >= $reqStudentTests);
+ $met = 0 unless $studentMet;
+ }
+ push @details, {
+ name => 'Student Tests',
+ met => $studentMet,
+ expected => $studentExpected,
+ actual => $actualStudentTests
+ };
+
+ my $refMet = 1;
+ my $refExpected = 'Not required';
+ if ($reqRefTests > 0)
+ {
+ $refExpected = ">= $reqRefTests";
+ $refMet = ($actualRefTests >= $reqRefTests);
+ $met = 0 unless $refMet;
+ }
+ push @details, {
+ name => 'Reference Tests',
+ met => $refMet,
+ expected => $refExpected,
+ actual => $actualRefTests
+ };
+
+ my $coverMet = 1;
+ my $coverExpected = 'Not required';
+ my $coverActual = sprintf('%.1f%%', $actualCoverage);
+ if ($reqCover > 0)
+ {
+ $coverExpected = ">= $reqCover%";
+ $coverMet = ($actualCoverage >= $reqCover);
+ $met = 0 unless $coverMet;
+ }
+ push @details, {
+ name => 'Mutation Coverage',
+ met => $coverMet,
+ expected => $coverExpected,
+ actual => $coverActual
+ };
+
+ my $styleMet = 1;
+ my $styleExpected = 'Not required';
+ my $styleActual = sprintf('%g', $actualStyle);
+ if ($reqStyle > 0)
+ {
+ $styleExpected = ">= $reqStyle";
+ $styleMet = ($actualStyle >= $reqStyle);
+ $met = 0 unless $styleMet;
+ }
+ push @details, {
+ name => 'Style Points',
+ met => $styleMet,
+ expected => $styleExpected,
+ actual => $styleActual
+ };
+
+ push @milestoneResults, {
+ id => "milestone$mNum",
+ name => "Milestone $mNum",
+ met => $met,
+ details => \@details
+ };
+}
+
+if ($milestoneCount > 0)
+{
+ my $totalMilestones = scalar @milestoneResults;
+ my $milestonesPassed = 0;
+
+ foreach my $milestoneResult (@milestoneResults)
+ {
+ $milestonesPassed++ if $milestoneResult->{met};
+ }
+
+ # Fallback to properties if detailed milestone results were not built.
+ if ($totalMilestones == 0)
+ {
+ for (my $i = 1; $i <= 20; $i++)
+
+ {
+ my $passedProp = $cfg->getProperty("milestonePassed.$i");
+ if (defined $passedProp)
+ {
+ $totalMilestones++;
+ if ($passedProp eq 'true' || $passedProp eq '1')
+ {
+ $milestonesPassed++;
+ }
+ }
+ }
+ }
+
+ my $sectionTitle = "Milestone Progress";
+ my $milestonePercent = 0;
+ my $milestonePercentKnown = 1;
+
+ if ($totalMilestones == 0)
+ {
+ $sectionTitle .= " (Unknown!)";
+ $milestonePercentKnown = 0;
+ }
+ elsif ($milestonesPassed >= $totalMilestones)
+ {
+ $sectionTitle .= " (100%)";
+ $milestonePercent = 100;
+ }
+ else
+ {
+ $milestonePercent =
+ int(($milestonesPassed / $totalMilestones) * 100.0 * 10 + 0.5) / 10;
+ if ($milestonePercent == 100)
+ {
+ # Don't show 100% if some milestones failed
+ $milestonePercent--;
+ }
+ $sectionTitle .= " ($milestonePercent%)";
+ }
+
+ $status{'feedback'}->startFeedbackSection(
+ $sectionTitle, ++$expSectionId,
+ $useEnhancedFeedback
+ || ($milestonePercentKnown && $milestonePercent >= 100));
+ $status{'feedback'}->print("
");
+
+ if ($totalMilestones == 0)
+ {
+ $status{'feedback'}->print(<No milestone configuration was detected.
+
This assignment may not have milestones configured, or the milestone
+properties could not be found.
+EOF
+ }
+ elsif ($milestonePercentKnown && $milestonePercent >= 100)
+ {
+ $status{'feedback'}->print(<Congratulations! You have met all $totalMilestones milestones for this assignment.
+EOF
+ }
+ else
+ {
+ $status{'feedback'}->print(<You have met $milestonesPassed out of $totalMilestones milestones.
+Review the details below to see which requirements need to be addressed.
+EOF
+ }
+
+ # Display detailed milestone information
+ if ($totalMilestones > 0)
+ {
+ $status{'feedback'}->print(
+ "
The following milestones are defined for this assignment:
\n");
+
+ if (@milestoneResults)
+ {
+ printMilestoneFeedbackDetails($status{'feedback'}, \@milestoneResults);
+ }
+ else
+ {
+ # Fallback when no detailed milestone results are available: show requirements only
+ $status{'feedback'}->print("
\n");
+ for (my $i = 1; $i <= $totalMilestones; $i++)
+ {
+ my $dueDateProp = $cfg->getProperty("milestoneDueDate.$i", '0');
+ my $dueTimeProp = $cfg->getProperty("milestoneDueTime.$i", '0');
+ my $passed = $cfg->getProperty("milestonePassed.$i", 'false');
+ my $mutationCovMin = $cfg->getProperty("milestoneMinMutationCoverage.$i", '0');
+ my $studentTestMin = $cfg->getProperty("milestoneMinStudentTests.$i", '0');
+ my $refTestMin = $cfg->getProperty("milestoneMinRefTests.$i", '0');
+ my $styleMin = $cfg->getProperty("milestoneStyleMin.$i", '0');
+
+ next unless milestoneHasConfiguredRequirements(
+ $dueDateProp, $dueTimeProp, $studentTestMin,
+ $refTestMin, $styleMin, $mutationCovMin);
+
+ my $class = ($passed eq 'true' || $passed eq '1') ? 'complete' : 'incomplete';
+ my $statusText = ($passed eq 'true' || $passed eq '1') ? 'Met' : 'Not Met';
+
+ $status{'feedback'}->print("
Your own software tests must be included for '
@@ -5732,7 +7323,8 @@ sub computeExpandSectionId
foreach my $errorStruct (@{$behaviorSectionExpanded{$element}})
{
print IMPROVEDFEEDBACKFILE '
\n";
my @linesOfCode = split /\n/, $errorStruct->linesOfCode;
@@ -5903,7 +7497,8 @@ sub computeExpandSectionId
if ($errorStruct->enhancedMessage)
{
print IMPROVEDFEEDBACKFILE '
',
- htmlEscape($errorStruct->enhancedMessage), '
';
+ smartHtmlEscapeAndPeel($errorStruct->enhancedMessage),
+ '';
}
}
}
@@ -5913,15 +7508,17 @@ sub computeExpandSectionId
END_MESSAGE
}
-}
+
print IMPROVEDFEEDBACKFILE "\n";
close IMPROVEDFEEDBACKFILE;
#print compilerErrorHintKey('final parameter abc may not be assigned');
+
+#=============================================================================
# PDF printout
-# -----------
+#=============================================================================
if (-f $pdfPrintout)
{
addReportFileWithStyle(
@@ -5934,14 +7531,282 @@ sub computeExpandSectionId
"PDF code printout");
}
+
+#=============================================================================
+# EMRN Scoring and Feedback
+#=============================================================================
+if ($useEMRN)
+{
+ my $maxPossible = $maxCorrectnessScore + $maxToolScore;
+ my $rawScore = $runtimeScore + $staticScore;
+ my $rawPct = $rawScore / $maxPossible;
+ my $subTime = $cfg->getProperty('submissionTimestamp', 0);
+ my $dueTime = $cfg->getProperty('dueDateTimestamp', $subTime);
+
+ my $emrnCmt = '';
+ my $emrnCategory = 'Not Assessable';
+# print <= 0.95
+ && $runtimeScore / $maxCorrectnessScore >= 0.95
+ && $staticScore / $maxToolScore >= 0.95
+ # && $subTime <= $dueTime
+ )
+ {
+ $emrnCategory = 'Excellent';
+ if ($runtimeScore < $maxCorrectnessScore || $staticScore < $maxToolScore)
+ {
+ $emrnCmt = 'Your submission passes most auto-grader checks and '
+ . 'appears to only have a few small issues remaining. To perfect '
+ . 'your work, '
+ . 'use the feedback below to identify any remaining areas for '
+ . 'improvement.';
+ }
+ else
+ {
+ $emrnCmt = 'Your submission passes all auto-grader checks for this '
+ . 'assignment.';
+ }
+ $staticScore = $maxToolScore
+ * 1.0 / ($maxToolScore + $maxCorrectnessScore)
+ * $emrnExcellent;
+ $runtimeScore = $maxCorrectnessScore
+ * 1.0 / ($maxToolScore + $maxCorrectnessScore)
+ * $emrnExcellent;
+ }
+
+ # Meets expectations
+ elsif ($rawPct >= 0.86
+ && $runtimeScore / $maxCorrectnessScore >= 0.86
+ && $staticScore / $maxToolScore >= 0.92)
+ {
+# if ($maxPossible < 100)
+# {
+ $staticScore = $maxToolScore
+ * 1.0 / ($maxToolScore + $maxCorrectnessScore)
+ * $emrnMeetsExpectations;
+ $runtimeScore = $maxCorrectnessScore
+ * 1.0 / ($maxToolScore + $maxCorrectnessScore)
+ * $emrnMeetsExpectations;
+# if (!$useEMRNManual)
+# {
+# $staticScore *= 10.0 / 11.0;
+# $runtimeScore *= 10.0 / 11.0;
+# }
+# }
+# elsif ($staticScore == $maxToolScore)
+# {
+# $runtimeScore = $maxPossible * 0.8 - $staticScore;
+# }
+# elsif ($runtimeScore == $maxCorrectnessScore)
+# {
+# $staticScore = $maxPossible * 0.8 - $runtimeScore;
+# }
+# else
+# {
+# $staticScore = 0.8 * $maxToolScore;
+# $runtimeScore = 0.8 * $maxCorrectnessScore;
+# }
+ $emrnCategory = 'Meets Expectations';
+ $emrnCmt = 'Your submission meets most expectations, but there are still '
+ . 'some areas where improvements can be made. See the feedback below '
+ . 'to address these issues.';
+ }
+ elsif ($rawPct > 0 && $runtimeScore > 0)
+ {
+# my $target = 10; # Should be programmable
+ $staticScore = $maxToolScore
+ * 1.0 / ($maxToolScore + $maxCorrectnessScore)
+ * $emrnRevisionNeeded;
+ $runtimeScore = $maxCorrectnessScore
+ * 1.0 / ($maxToolScore + $maxCorrectnessScore)
+ * $emrnRevisionNeeded;
+# if (!$useEMRNManual)
+# {
+# $staticScore *= 1.0 / 10.0;
+# $runtimeScore *= 1.0 / 10.0;
+# }
+
+# if ($staticScore == $maxToolScore)
+# {
+# $runtimeScore = $target - $staticScore;
+# }
+# elsif ($runtimeScore == $maxCorrectnessScore)
+# {
+# $staticScore = $target - $runtimeScore;
+# }
+# else
+# {
+# $staticScore = $target / 2;
+# $runtimeScore = $target / 2;
+# }
+ $emrnCategory = 'Revision Needed';
+ $emrnCmt = 'Your submission meets some expectations, but there are still '
+ . 'some areas where improvements are needed. See the feedback below '
+ . 'to address these issues.';
+ }
+ else
+ {
+ $runtimeScore = 0;
+ $staticScore = 0;
+ $emrnCategory = 'Not Assessable';
+ $emrnCmt = 'Your submission cannot be assessed effectively. See the '
+ . 'feedback below to identify the issues that need to be addressed.';
+ }
+
+ print "EMRN category = $emrnCategory\n" if ($debug > 1);
+ my $emrnLetter = substr($emrnCategory, 0, 1);
+ $cfg->setProperty('score.category', $emrnCategory);
+ if ($useEMRNManual)
+ {
+ $emrnCmt .= ' Be sure to double-check the assignment\'s manually '
+ . 'graded criteria yourself.';
+ }
+
+ my $emrnFileName = "$resultDir/emrn.html";
+ open(EMRNFEEDBACKFILE, ">$emrnFileName")
+ || croak "Cannot open '$emrnFileName' for writing: $!";
+
+ print EMRNFEEDBACKFILE <
+
+
Auto-Grader Criteria: ($emrnLetter) $emrnCategory
+
$emrnCmt
+
+
+END_MESSAGE
+ close(EMRNFEEDBACKFILE);
+}
+
+
+my @milestone_results = ();
+
+for (my $i = 0; $i < $milestoneCount; $i++) {
+ my $mNum = defined($milestoneNumbers[$i]) ? $milestoneNumbers[$i] : ($i + 1);
+ my $dueDate = $milestoneDueDatesTimestamps[$i];
+ my $status = "IN PROGRESS";
+
+ # Requirements
+ my $reqStudentTests = $milestoneMinStudentTests[$i] // 0;
+ my $reqRefTests = $milestoneMinRefTests[$i] //0;
+ my $reqCover = $milestoneMinMutationCoverages[$i] // 0;
+ my $reqStyle = $milestoneStyleMins[$i] // 0;
+
+ next unless milestoneHasConfiguredRequirements(
+ $cfg->getProperty("milestoneDueDate.$mNum"),
+ $cfg->getProperty("milestoneDueTime.$mNum"),
+ $reqStudentTests, $reqRefTests, $reqStyle, $reqCover);
+
+ my $actualStudentTests = 0;
+ my $actualRefTests = 0;
+ my $actualCoverage = 0;
+ my $actualStyle = 0;
+
+
+
+ if ($milestoneAlreadyPassed[$i]) {
+ # SKIP THE CHECK: They already passed it!
+ $status = "ACHIEVED";
+
+ $actualStudentTests = $reqStudentTests;
+ $actualRefTests = $reqRefTests;
+ $actualCoverage = $reqCover;
+ $actualStyle = $reqStyle;
+
+ } else {
+ # Actuals
+ $actualStudentTests = (defined $status{'validateTestResults'}) ? ($status{'validateTestResults'}->testsExecuted - $status{'validateTestResults'}->testsFailed) : 0;
+ $actualRefTests =
+ (defined $instructorCasesPercent
+ && $instructorCasesPercent =~ /^\d+(?:\.\d+)?$/)
+ ? $instructorCasesPercent
+ : 0;
+ $actualCoverage = $testingSectionStatus{'codeCoveragePercent'};
+ $actualStyle = defined $styleSectionStatus{'pointsGained'}
+ ? $styleSectionStatus{'pointsGained'}
+ : $styleSectionStatus{'pointsGainedPercent'};
+
+ # Determine Status
+ my $met = ($actualStudentTests >= $reqStudentTests && $actualCoverage >= $reqCover && $actualStyle >= $reqStyle && $actualRefTests >= $reqRefTests) ? 1 : 0;
+
+ if ($met) {
+ $status = "ACHIEVED";
+ $cfg->setProperty("milestonePassed.$mNum", "true");
+ } elsif (defined($dueDate)
+ && $dueDate > 0
+ && $cfg->getProperty('submissionTimestamp', 0) > $dueDate) {
+ $status = "MISSED";
+ }
+ }
+
+ # 2. Build the json for this milestone
+ my %milestone_entry = (
+ milestoneNumber => $mNum,
+ dueDate => (defined($dueDate) ? $dueDate : 0),
+ status => $status,
+ requirements => {
+ minStudentTests => int($reqStudentTests),
+ minRefTests => int($reqRefTests),
+ minCover => int($reqCover),
+ minStyle => int($reqStyle)
+ },
+ actuals => {
+ studentTests => $actualStudentTests,
+ referenceTests => $actualRefTests,
+ cover => $actualCoverage,
+ style => $actualStyle
+ }
+ );
+
+ push @milestone_results, \%milestone_entry;
+}
+
+# 3. Write to milestones.json
+my $json_output = {
+ submissionDate => $cfg->getProperty('submissionTimestamp', 0),
+ milestones => \@milestone_results
+};
+
+# Encode and write
+eval {
+ require JSON::PP;
+ my $json_text = JSON::PP->new->utf8->pretty->encode($json_output);
+ my $milestoneFilename = "$resultDir/milestones.json";
+
+ open(my $fh, '>', $milestoneFilename) or die "Could not open '$milestoneFilename' for writing: $!";
+ print $fh $json_text;
+ close $fh;
+ print "Successfully wrote milestone data to $milestoneFilename\n" if $debug;
+};
+if ($@) {
+ print "Error generating JSON: $@" if $debug;
+}
+
+
+#=============================================================================
# Script log
-# ----------
+#=============================================================================
if (-f $scriptLog && stat($scriptLog)->size > 0)
{
addReportFileWithStyle($cfg, $scriptLogRelative, "text/plain", 0, "admin");
addReportFileWithStyle($cfg, $antLogRelative, "text/plain", 0, "admin");
}
+
+#=============================================================================
+# Save score and rewrite properties
+#=============================================================================
$cfg->setProperty('score.correctness', $runtimeScore);
$cfg->setProperty('score.tools', $staticScore );
$cfg->setProperty('expSectionId', $expSectionId);
diff --git a/src/perllib/File/Slurp.pm b/src/perllib/File/Slurp.pm
new file mode 100644
index 0000000..e5329b1
--- /dev/null
+++ b/src/perllib/File/Slurp.pm
@@ -0,0 +1,1128 @@
+package File::Slurp;
+
+use strict;
+use warnings ;
+
+our $VERSION = '9999.32';
+$VERSION = eval $VERSION;
+
+use Carp ;
+use Exporter qw(import);
+use Fcntl qw( :DEFAULT ) ;
+use File::Basename ();
+use File::Spec;
+use File::Temp qw(tempfile);
+use IO::Handle ();
+use POSIX qw( :fcntl_h ) ;
+use Errno ;
+
+my @std_export = qw(
+ read_file
+ write_file
+ overwrite_file
+ append_file
+ read_dir
+) ;
+
+my @edit_export = qw(
+ edit_file
+ edit_file_lines
+) ;
+
+my @abbrev_export = qw(
+ rf
+ wf
+ ef
+ efl
+) ;
+
+our @EXPORT_OK = (
+ @edit_export,
+ @abbrev_export,
+ qw(
+ slurp
+ prepend_file
+ ),
+) ;
+
+our %EXPORT_TAGS = (
+ 'all' => [ @std_export, @edit_export, @abbrev_export, @EXPORT_OK ],
+ 'edit' => [ @edit_export ],
+ 'std' => [ @std_export ],
+ 'abr' => [ @abbrev_export ],
+) ;
+
+our @EXPORT = @std_export ;
+
+my $max_fast_slurp_size = 1024 * 100 ;
+
+my $is_win32 = $^O =~ /win32/i ;
+
+*slurp = \&read_file ;
+*rf = \&read_file ;
+
+sub read_file {
+ my $file_name = shift;
+ my $opts = (ref $_[0] eq 'HASH') ? shift : {@_};
+ # options we care about:
+ # array_ref binmode blk_size buf_ref chomp err_mode scalar_ref
+
+ # let's see if we have a stringified object before doing anything else
+ # We then only have to deal with when we are given a file handle/globref
+ if (ref($file_name)) {
+ my $ref_result = _check_ref($file_name, $opts);
+ if (ref($ref_result)) {
+ @_ = ($opts, $ref_result);
+ goto &_error;
+ }
+ $file_name = $ref_result if $ref_result;
+ # we have now stringified $file_name if possible. if it's still a ref
+ # then we probably have a file handle
+ }
+
+ my $fh;
+ if (ref($file_name)) {
+ $fh = $file_name;
+ }
+ else {
+ # to keep with the old ways, read in :raw by default
+ unless (open $fh, "<:raw", $file_name) {
+ @_ = ($opts, "read_file '$file_name' - open: $!");
+ goto &_error;
+ }
+ # even though we set raw, let binmode take place here (busted)
+ if (my $bm = $opts->{binmode}) {
+ binmode $fh, $bm;
+ }
+ }
+
+ # we are now sure to have an open file handle. Let's slurp it in the same
+ # way that File::Slurper does.
+ my $buf;
+ my $buf_ref = $opts->{buf_ref} || \$buf;
+ ${$buf_ref} = '';
+ my $blk_size = $opts->{blk_size} || 1024 * 1024;
+ if (my $size = -f $fh && -s _) {
+ $blk_size = $size if $size < $blk_size;
+ my ($pos, $read) = 0;
+ do {
+ unless(defined($read = read $fh, ${$buf_ref}, $blk_size, $pos)) {
+ @_ = ($opts, "read_file '$file_name' - read: $!");
+ goto &_error;
+ }
+ $pos += $read;
+ } while ($read && $pos < $size);
+ }
+ else {
+ ${$buf_ref} = do { local $/; <$fh> };
+ }
+ seek($fh, $opts->{_data_tell}, SEEK_SET) if $opts->{_is_data} && $opts->{_data_tell};
+
+ # line endings if we're on Windows
+ ${$buf_ref} =~ s/\015\012/\012/g if ${$buf_ref} && $is_win32 && !$opts->{binmode};
+
+ # we now have a buffer filled with the file content. Figure out how to
+ # return it to the user
+ my $want_array = wantarray; # let's only ask for this once
+ if ($want_array || $opts->{array_ref}) {
+ use re 'taint';
+ my $sep = $/;
+ $sep = '\n\n+' if defined $sep && $sep eq '';
+ # split the buffered content into lines
+ my @lines = length(${$buf_ref}) ?
+ ${$buf_ref} =~ /(.*?$sep|.+)/sg : ();
+ chomp @lines if $opts->{chomp};
+ return \@lines if $opts->{array_ref};
+ return @lines;
+ }
+ return $buf_ref if $opts->{scalar_ref};
+ # if the function was called in scalar context, return the contents
+ return ${$buf_ref} if defined $want_array;
+ # if we were called in void context, return nothing
+ return;
+}
+
+# errors in this sub are returned as scalar refs
+# a normal IO/GLOB handle is an empty return
+# an overloaded object returns its stringified as a scalarfilename
+
+sub _check_ref {
+
+ my( $handle, $opts ) = @_ ;
+
+# check if we are reading from a handle (GLOB or IO object)
+
+ if ( eval { $handle->isa( 'GLOB' ) || $handle->isa( 'IO' ) } ) {
+
+# we have a handle. deal with seeking to it if it is DATA
+
+ my $err = _seek_data_handle( $handle, $opts ) ;
+
+# return the error string if any
+
+ return \$err if $err ;
+
+# we have good handle
+ return ;
+ }
+
+ eval { require overload } ;
+
+# return an error if we can't load the overload pragma
+# or if the object isn't overloaded
+
+ return \"Bad handle '$handle' is not a GLOB or IO object or overloaded"
+ if $@ || !overload::Overloaded( $handle ) ;
+
+# must be overloaded so return its stringified value
+
+ return "$handle" ;
+}
+
+sub _seek_data_handle {
+
+ my( $handle, $opts ) = @_ ;
+ # store some meta-data about the __DATA__ file handle
+ $opts->{_is_data} = 0;
+ $opts->{_data_tell} = 0;
+
+# DEEP DARK MAGIC. this checks the UNTAINT IO flag of a
+# glob/handle. only the DATA handle is untainted (since it is from
+# trusted data in the source file). this allows us to test if this is
+# the DATA handle and then to do a sysseek to make sure it gets
+# slurped correctly. on some systems, the buffered i/o pointer is not
+# left at the same place as the fd pointer. this sysseek makes them
+# the same so slurping with sysread will work.
+
+ eval{ require B } ;
+
+ if ( $@ ) {
+
+ return <IO->IoFLAGS & 16 ) {
+
+ # we now know we have the data handle. Let's store its original
+ # location in the file so that we can put it back after the read.
+ # this is only done for Bugwards-compatibility in some dists such as
+ # CPAN::Index::API that made use of the oddity where sysread was in use
+ # before
+ $opts->{_is_data} = 1;
+ $opts->{_data_tell} = tell($handle);
+# set the seek position to the current tell.
+
+ # unless( sysseek( $handle, tell( $handle ), SEEK_SET ) ) {
+ # return "read_file '$handle' - sysseek: $!" ;
+ # }
+ }
+
+# seek was successful, return no error string
+
+ return ;
+}
+
+*wf = \&write_file ;
+
+sub write_file {
+ my $file_name = shift;
+ my $opts = (ref $_[0] eq 'HASH') ? shift : {};
+ # options we care about:
+ # append atomic binmode buf_ref err_mode no_clobber perms
+
+ my $fh;
+ my $no_truncate = 0;
+ my $orig_filename;
+ # let's see if we have a stringified object or some sort of handle
+ # or globref before doing anything else
+ if (ref($file_name)) {
+ my $ref_result = _check_ref($file_name, $opts);
+ if (ref($ref_result)) {
+ # some error happened while checking for a ref
+ @_ = ($opts, $ref_result);
+ goto &_error;
+ }
+ if ($ref_result) {
+ # we have now stringified $file_name from the overloaded obj
+ $file_name = $ref_result;
+ }
+ else {
+ # we now have a proper handle ref
+ # make sure we don't call truncate on it
+ $fh = $file_name;
+ $no_truncate = 1;
+ # can't do atomic or permissions on a file handle
+ delete $opts->{atomic};
+ delete $opts->{perms};
+ }
+ }
+
+ # open the file for writing if we were given a filename
+ unless ($fh) {
+ $orig_filename = $file_name;
+ my $perms = defined($opts->{perms}) ? $opts->{perms} : 0666;
+ # set the mode for the sysopen
+ my $mode = O_WRONLY | O_CREAT;
+ $mode |= O_APPEND if $opts->{append};
+ $mode |= O_EXCL if $opts->{no_clobber};
+ if ($opts->{atomic}) {
+ # in an atomic write, we must open a new file in the same directory
+ # as the original to account for ACLs. We must also set the new file
+ # to the same permissions as the original unless overridden by the
+ # caller's request to set a specified permission set.
+ my $dir = File::Spec->rel2abs(File::Basename::dirname($file_name));
+ if (!defined($opts->{perms}) && -e $file_name && -f _) {
+ $perms = 07777 & (stat $file_name)[2];
+ }
+ # we must ensure we're using a good temporary filename (doesn't already
+ # exist). This is slower, but safer.
+ {
+ local $^W = 0; # AYFKM
+ (undef, $file_name) = tempfile('.tempXXXXX', DIR => $dir, OPEN => 0);
+ }
+ }
+ $fh = local *FH;
+ unless (sysopen($fh, $file_name, $mode, $perms)) {
+ @_ = ($opts, "write_file '$file_name' - sysopen: $!");
+ goto &_error;
+ }
+ }
+ # we now have an open file handle as well as data to write to that handle
+ if (my $binmode = $opts->{binmode}) {
+ binmode($fh, $binmode);
+ }
+
+ # get the data to print to the file
+ # get the buffer ref - it depends on how the data is passed in
+ # after this if/else $buf_ref will have a scalar ref to the data
+ my $buf_ref;
+ my $data_is_ref = 0;
+ if (ref($opts->{buf_ref}) eq 'SCALAR') {
+ # a scalar ref passed in %opts has the data
+ # note that the data was passed by ref
+ $buf_ref = $opts->{buf_ref};
+ $data_is_ref = 1;
+ }
+ elsif (ref($_[0]) eq 'SCALAR') {
+ # the first value in @_ is the scalar ref to the data
+ # note that the data was passed by ref
+ $buf_ref = shift;
+ $data_is_ref = 1;
+ }
+ elsif (ref($_[0]) eq 'ARRAY') {
+ # the first value in @_ is the array ref to the data so join it.
+ ${$buf_ref} = join '', @{$_[0]};
+ }
+ else {
+ # good old @_ has all the data so join it.
+ ${$buf_ref} = join '', @_;
+ }
+
+ # seek and print
+ seek($fh, 0, SEEK_END) if $opts->{append};
+ print {$fh} ${$buf_ref};
+ truncate($fh, tell($fh)) unless $no_truncate;
+ close($fh);
+
+ if ($opts->{atomic} && !rename($file_name, $orig_filename)) {
+ @_ = ($opts, "write_file '$file_name' - rename: $!");
+ goto &_error;
+ }
+
+ return 1;
+}
+
+# this is for backwards compatibility with the previous File::Slurp module.
+# write_file always overwrites an existing file
+*overwrite_file = \&write_file ;
+
+# the current write_file has an append mode so we use that. this
+# supports the same API with an optional second argument which is a
+# hash ref of options.
+
+sub append_file {
+
+# get the optional opts hash ref
+ my $opts = $_[1] ;
+ if ( ref $opts eq 'HASH' ) {
+
+# we were passed an opts ref so just mark the append mode
+
+ $opts->{append} = 1 ;
+ }
+ else {
+
+# no opts hash so insert one with the append mode
+
+ splice( @_, 1, 0, { append => 1 } ) ;
+ }
+
+# magic goto the main write_file sub. this overlays the sub without touching
+# the stack or @_
+
+ goto &write_file
+}
+
+# prepend data to the beginning of a file
+
+sub prepend_file {
+
+ my $file_name = shift ;
+
+#print "FILE $file_name\n" ;
+
+ my $opts = ( ref $_[0] eq 'HASH' ) ? shift : {} ;
+
+# delete unsupported options
+
+ my @bad_opts =
+ grep $_ ne 'err_mode' && $_ ne 'binmode', keys %{$opts} ;
+
+ delete @{$opts}{@bad_opts} ;
+
+ my $prepend_data = shift ;
+ $prepend_data = '' unless defined $prepend_data ;
+ $prepend_data = ${$prepend_data} if ref $prepend_data eq 'SCALAR' ;
+
+#print "PRE [$prepend_data]\n" ;
+
+ my $err_mode = delete $opts->{err_mode} ;
+ $opts->{ err_mode } = 'croak' ;
+ $opts->{ scalar_ref } = 1 ;
+
+ my $existing_data = eval { read_file( $file_name, $opts ) } ;
+
+ if ( $@ ) {
+
+ @_ = ( { err_mode => $err_mode },
+ "prepend_file '$file_name' - read_file: $!" ) ;
+ goto &_error ;
+ }
+
+#print "EXIST [$$existing_data]\n" ;
+
+ $opts->{atomic} = 1 ;
+ my $write_result =
+ eval { write_file( $file_name, $opts,
+ $prepend_data, $$existing_data ) ;
+ } ;
+
+ if ( $@ ) {
+
+ @_ = ( { err_mode => $err_mode },
+ "prepend_file '$file_name' - write_file: $!" ) ;
+ goto &_error ;
+ }
+
+ return $write_result ;
+}
+
+# edit a file as a scalar in $_
+
+*ef = \&edit_file ;
+
+sub edit_file(&$;$) {
+
+ my( $edit_code, $file_name, $opts ) = @_ ;
+ $opts = {} unless ref $opts eq 'HASH' ;
+
+# my $edit_code = shift ;
+# my $file_name = shift ;
+# my $opts = ( ref $_[0] eq 'HASH' ) ? shift : {} ;
+
+#print "FILE $file_name\n" ;
+
+# delete unsupported options
+
+ my @bad_opts =
+ grep $_ ne 'err_mode' && $_ ne 'binmode', keys %{$opts} ;
+
+ delete @{$opts}{@bad_opts} ;
+
+# keep the user err_mode and force croaking on internal errors
+
+ my $err_mode = delete $opts->{err_mode} ;
+ $opts->{ err_mode } = 'croak' ;
+
+# get a scalar ref for speed and slurp the file into a scalar
+
+ $opts->{ scalar_ref } = 1 ;
+ my $existing_data = eval { read_file( $file_name, $opts ) } ;
+
+ if ( $@ ) {
+
+ @_ = ( { err_mode => $err_mode },
+ "edit_file '$file_name' - read_file: $!" ) ;
+ goto &_error ;
+ }
+
+#print "EXIST [$$existing_data]\n" ;
+
+ my( $edited_data ) = map { $edit_code->(); $_ } $$existing_data ;
+
+ $opts->{atomic} = 1 ;
+ my $write_result =
+ eval { write_file( $file_name, $opts, $edited_data ) } ;
+
+ if ( $@ ) {
+
+ @_ = ( { err_mode => $err_mode },
+ "edit_file '$file_name' - write_file: $!" ) ;
+ goto &_error ;
+ }
+
+ return $write_result ;
+}
+
+*efl = \&edit_file_lines ;
+
+sub edit_file_lines(&$;$) {
+
+ my( $edit_code, $file_name, $opts ) = @_ ;
+ $opts = {} unless ref $opts eq 'HASH' ;
+
+# my $edit_code = shift ;
+# my $file_name = shift ;
+# my $opts = ( ref $_[0] eq 'HASH' ) ? shift : {} ;
+
+#print "FILE $file_name\n" ;
+
+# delete unsupported options
+
+ my @bad_opts =
+ grep $_ ne 'err_mode' && $_ ne 'binmode', keys %{$opts} ;
+
+ delete @{$opts}{@bad_opts} ;
+
+# keep the user err_mode and force croaking on internal errors
+
+ my $err_mode = delete $opts->{err_mode} ;
+ $opts->{ err_mode } = 'croak' ;
+
+# get an array ref for speed and slurp the file into lines
+
+ $opts->{ array_ref } = 1 ;
+ my $existing_data = eval { read_file( $file_name, $opts ) } ;
+
+ if ( $@ ) {
+
+ @_ = ( { err_mode => $err_mode },
+ "edit_file_lines '$file_name' - read_file: $!" ) ;
+ goto &_error ;
+ }
+
+#print "EXIST [$$existing_data]\n" ;
+
+ my @edited_data = map { $edit_code->(); $_ } @$existing_data ;
+
+ $opts->{atomic} = 1 ;
+ my $write_result =
+ eval { write_file( $file_name, $opts, @edited_data ) } ;
+
+ if ( $@ ) {
+
+ @_ = ( { err_mode => $err_mode },
+ "edit_file_lines '$file_name' - write_file: $!" ) ;
+ goto &_error ;
+ }
+
+ return $write_result ;
+}
+
+# basic wrapper around opendir/readdir
+
+sub read_dir {
+
+ my $dir = shift ;
+ my $opts = ( ref $_[0] eq 'HASH' ) ? shift : { @_ } ;
+
+# this handle will be destroyed upon return
+
+ local(*DIRH);
+
+# open the dir and handle any errors
+
+ unless ( opendir( DIRH, $dir ) ) {
+
+ @_ = ( $opts, "read_dir '$dir' - opendir: $!" ) ;
+ goto &_error ;
+ }
+
+ my @dir_entries = readdir(DIRH) ;
+
+ @dir_entries = grep( $_ ne "." && $_ ne "..", @dir_entries )
+ unless $opts->{'keep_dot_dot'} ;
+
+ if ( $opts->{'prefix'} ) {
+
+ $_ = File::Spec->catfile($dir, $_) for @dir_entries;
+ }
+
+ return @dir_entries if wantarray ;
+ return \@dir_entries ;
+}
+
+# error handling section
+#
+# all the error handling uses magic goto so the caller will get the
+# error message as if from their code and not this module. if we just
+# did a call on the error code, the carp/croak would report it from
+# this module since the error sub is one level down on the call stack
+# from read_file/write_file/read_dir.
+
+
+my %err_func = (
+ 'carp' => \&carp,
+ 'croak' => \&croak,
+) ;
+
+sub _error {
+
+ my( $opts, $err_msg ) = @_ ;
+
+# get the error function to use
+
+ my $func = $err_func{ $opts->{'err_mode'} || 'croak' } ;
+
+# if we didn't find it in our error function hash, they must have set
+# it to quiet and we don't do anything.
+
+ return unless $func ;
+
+# call the carp/croak function
+
+ $func->($err_msg) if $func ;
+
+# return a hard undef (in list context this will be a single value of
+# undef which is not a legal in-band value)
+
+ return undef ;
+}
+
+1;
+__END__
+
+=head1 NAME
+
+File::Slurp - Simple and Efficient Reading/Writing/Modifying of Complete Files
+
+=head1 SYNOPSIS
+
+ use File::Slurp;
+
+ # read in a whole file into a scalar
+ my $text = read_file('/path/file');
+
+ # read in a whole file into an array of lines
+ my @lines = read_file('/path/file');
+
+ # write out a whole file from a scalar
+ write_file('/path/file', $text);
+
+ # write out a whole file from an array of lines
+ write_file('/path/file', @lines);
+
+ # Here is a simple and fast way to load and save a simple config file
+ # made of key=value lines.
+ my %conf = read_file('/path/file') =~ /^(\w+)=(.*)$/mg;
+ write_file('/path/file', {atomic => 1}, map "$_=$conf{$_}\n", keys %conf);
+
+ # insert text at the beginning of a file
+ prepend_file('/path/file', $text);
+
+ # in-place edit to replace all 'foo' with 'bar' in file
+ edit_file { s/foo/bar/g } '/path/file';
+
+ # in-place edit to delete all lines with 'foo' from file
+ edit_file_lines sub { $_ = '' if /foo/ }, '/path/file';
+
+ # read in a whole directory of file names (skipping . and ..)
+ my @files = read_dir('/path/to/dir');
+
+=head1 DESCRIPTION
+
+This module provides subs that allow you to read or write entire files
+with one simple call. They are designed to be simple to use, have
+flexible ways to pass in or get the file contents and to be very
+efficient. There is also a sub to read in all the files in a
+directory.
+
+=head2 WARNING - PENDING DOOM
+
+Although you technically I, do NOT use this module to work on file handles,
+pipes, sockets, standard IO, or the C handle. These are
+features implemented long ago that just really shouldn't be abused here.
+
+Be warned: this activity will lead to inaccurate encoding/decoding of data.
+
+All further mentions of actions on the above have been removed from this
+documentation and that feature set will likely be deprecated in the future.
+
+In other words, if you don't have a filename to pass, consider using the
+standard C<< do { local $/; <$fh> } >>, or
+L/L for working with C<__DATA__>.
+
+=head1 FUNCTIONS
+
+L implements the following functions.
+
+=head2 append_file
+
+ use File::Slurp qw(append_file write_file);
+ my $res = append_file('/path/file', "Some text");
+ # same as
+ my $res = write_file('/path/file', {append => 1}, "Some text");
+
+The C function is simply a synonym for the
+L function, but ensures that the C option is
+set.
+
+=head2 edit_file
+
+ use File::Slurp qw(edit_file);
+ # perl -0777 -pi -e 's/foo/bar/g' /path/file
+ edit_file { s/foo/bar/g } '/path/file';
+ edit_file sub { s/foo/bar/g }, '/path/file';
+ sub replace_foo { s/foo/bar/g }
+ edit_file \&replace_foo, '/path/file';
+
+The C function reads in a file into C<$_>, executes a code block that
+should modify C<$_>, and then writes C<$_> back to the file. The C
+function reads in the entire file and calls the code block one time. It is
+equivalent to the C<-pi> command line options of Perl but you can call it from
+inside your program and not have to fork out a process.
+
+The first argument to C is a code block or a code reference. The
+code block is not followed by a comma (as with C and C