Regenerate multiple files
This is sort of a minor codicil to a handful of much more interesting posts about automating the generation of Daredevils RPG character sheets over on the Biblyon Broadsheet.
At a role-playing game convention recently, I had a whole bunch of pregenerated characters, and regularly needed to recreate their character sheets from source files after making changes to either the characters or to how I interpreted the character generation rules. I used a Daredevils pregen calculator to do this. But while the calculator could take multiple inputs and produce a single file with every character in it, I ended up choosing to have a separate output file for each character. It was easier to find any particular character that way.
This meant having to rerun the script for multiple characters every time I made changes. That’s obviously a target for automation. What I needed was a script to check each source file against its generated character sheet—and against the ever-changing daredevils script—and run the script on that source file if it’s newer than the sheet or if the destination is older than the script that generated it.
The “file” command could probably handle this for me. But it is an arcane command which is not amenable to obviousness. It produces long command lines with obscure switches that require, for me at least, reading through the man page every time I need to alter it. I need more obviousness in my automations.1
So I wrote a script that takes files on the command line, a directory for the output files, and a script to run on each file.
The basic requirements are:
- a bunch of input files, such as my character stats files for Daredevils;
- the same number of output files in a separate but common directory, with the same name (sans any extension) and the extension “.txt”;2
- a script that runs on the input files and produces text to standard output; the standard output for an input file is piped to the corresponding output
.txt
file.
The script checks whether the input file is younger than the output file before running or if the output file is older than the script. In the former case, the data has changed and the output needs to be recreated. In the latter case, the logic has changed, and the output needs to be recreated.
I haven’t needed a --force
option yet, but it’s an obvious potential add.
[toggle code]
- #!/usr/bin/perl
- # run a script on every file on the command line, piping it to a different folder and the same name.txt
- # if the script or the file has changed since the last run
- # Jerry Stratton astoundingscripts.com
- use File::Basename;
-
while ($option = shift) {
-
if ($option eq '--help') {
- help();
-
} elsif ($option eq '--commit') {
- $commit = 1;
-
} elsif (-d $option) {
- help("Two directories specified ($outputDirectory:$option)") if $outputDirectory;
- $outputDirectory = $option;
-
} elsif (-f $option) {
- $files[$#files+1] = $option;
-
} elsif ($scriptPath = `which $option`) {
- help("Two scripts specified ($script:$option)") if $script;
- chomp $scriptPath;
- $script = $option;
-
} else {
- help("Unknown option $option");
- }
-
if ($option eq '--help') {
- }
- help('No output directory specified') if !$outputDirectory;
- help('No script specified') if !$script;
- help('No files specified') if !@files;
-
foreach $source (@files) {
- ($sourceName, $folder, $extension) = fileparse($source, qr/\.[^.]+/);
- $outputPath = "$outputDirectory/$sourceName.txt";
- next if -f $outputPath && -M $source >= -M $outputPath && -M $outputPath <= -M $scriptPath;
- $source = quotemeta($source);
- $outputPath = quotemeta($outputPath);
- $command = "$script $source > $outputPath";
-
if ($commit) {
- print `$command`;
-
} else {
- print "$command\n";
- }
- }
-
sub help {
- my $message = shift;
- print "$0 [--help] [--commit] <outputDir> <script> <filename> [filenames]\n";
- print "Run <script> on every <filename>, piping output to <outDir> as a .txt file.\n";
- print "\t--help:\tthis help text.\n";
- print "\t--commit:\tperform script.\n";
- print "\toutputDir:\tthe directory to pipe output to, using the source file's filename.txt.\n";
- print "\tscript:\tthe script to run on each file, whose output is piped.\n";
- print "\tfilenames:\tthe filenames to run the script on, and whose name produces the output filename with .txt as the extension.\n";
- print "\n$message.\n" if $message;
- exit;
- }
If the input filename is X.ext
, the output filename is X.txt
. Since the entire purpose of this script is to regenerate the output files, any existing files in the output directory will be erased if they are younger than an input file with the same name. Take that as a warning: run this script carefully. That’s why I have the --commit
switch; the first time I run the script, I want to see what it’s going to do. Only then do I actually recreate the destination files.
Here’s an example of how I used it for the Kolchak game:
- pipewalk characters/*.txt data daredevils --commit
The character source files were all in a folder called “characters”. I sent the character sheets to a folder called “data” (because they were meant for pulling in to Scribus to create character sheet PDFs). And the script to be run on each source file is daredevils
.
The script does some sanity checking. It makes sure that the files exist. It makes sure that the output directory exists. It does not create the output directory if it doesn’t exist, since the entire point of this script is regenerating existing files. In the main use case so far, the Kolchak game, I’d already created individual files one by one as I built up my stable of pregenerated characters.
And, finally, it checks that there is a script with the specified name somewhere on your path. So you can use this for built-in commands as well. Suppose you’re keeping a collection of text files reversed. You might use this command for it:
- pipewalk *.txt redrum rev
The script detects that the rev
command exists; it detects that the redrum
folder exists; and it runs every file ending in .txt in the current directory through rev
and outputs the results to redrum
.
The script doesn’t care about command-line switches or full paths; as long as which
can find the part before the space, it will count as a script.
- pipewalk *.txt yells "~/bin/case --upper"
This runs every .txt
file in the current directory through a custom script called case
, with the switch --upper
.
Each of those examples needs --commit
to actually regenerate the files.
I have the same problem with
↑awk
. For me it’s usually easier in the long run to write a simple Perl script to handle the casesawk
is great at.Obviously the latter could be altered using a command-line switch if it became necessary.
↑
- Automated Scribus Daredevils NPC character sheets
- Part 2 of 2: the free Scribus page layout software makes it easy to automate the creation of NPC character sheets.
- The Biblyon Broadsheet
- Like adventurers of old you will delve into forgotten tombs where creatures of myth stalk the darkness. You will search uncharted wilderness for lost knowledge and hidden treasure. Where the hand-scrawled sign warns “beyond here lie dragons,” your stories begin.
- Daredevils NPC generator
- Part 1 of 2: a script to calculate Daredevils attributes, talents, skills, and stats from a text file of initial values and character development.
More Programming for Gamers
- Are my dice random?
- My d20 appears to have been rolling a lot of ones, a disaster if I were playing D&D but a boon for Gods & Monsters. Is my die really random, or is it skewed towards a particular result? Use the ‘R’ open source statistics tool to find out.
- Programming for Gamers: Choosing a random item
- If you can understand a roleplaying game’s rules, you can understand programming. Programming is a lot easier.
- Easier random tables
- Rather than having to type --table and --count, why not just type the table name and an optional count number?
- Programming a Roman thumb
- Before we move on to more complex stuff with the “random” script, how about something even simpler? Choose or die, Bezonian!
- Multiple tables on the same command
- The way the “random” script currently stands, it does one table at a time. Often, however, you have more than one table you know you’re going to need. Why not use one command to rule them all?
- 12 more pages with the topic Programming for Gamers, and other related pages