Skip to main content

Pascal Script: Examples

TODO: Review, tests all scripts.

This page provides practical examples demonstrating how to use the Pascal Script rule in ReNamer. Each example is self-contained and focuses on a specific task or concept. For function signatures and type details, refer to the Functions and Types reference articles.

Modifying FileName

The FileName variable is a WideString representing the new name for the current file, including the extension. Modifying it is the fundamental operation of any script. Assigning an empty string or raising an error causes the file to be skipped during renaming.

To add a prefix to every filename:

begin
  FileName := 'MyPrefix_' + FileName;
end.

To add a suffix before the extension, split the base name and extension apart first:

var
  BaseName, Ext: WideString;
begin
  BaseName := WideExtractBaseName(FileName);
  Ext := WideExtractFileExt(FileName);
  FileName := BaseName + '_backup' + Ext;
end.

WideExtractBaseName returns the filename without path or extension. WideExtractFileExt returns the extension including the leading dot (e.g. .txt). This split-and-rebuild pattern appears throughout many scripts whenever the extension must be preserved while transforming only the base name.

Accessing FilePath and path components

The FilePath constant holds the original full path to the current file (e.g. C:\Music\Artist\Album\Track.flac). It is read-only, but is invaluable for reading file content, extracting metadata, and deriving folder-based names.

To prefix a filename with its immediate parent folder name:

begin
  FileName := WideExtractFileName(WideExtractFileDir(FilePath)) + ' - ' + FileName;
end.

WideExtractFileDir returns the directory without a trailing backslash, and WideExtractFileName extracts just the last component — the immediate parent folder name.

For a deeper hierarchy, use WideSplitString to break the path into individual folder components:

var
  Parts: TWideStringArray;
begin
  Parts := WideSplitString(WideExtractFileDir(FilePath), '\');
  // Parts[Length(Parts) - 1] is the immediate parent folder
  // Parts[Length(Parts) - 2] is one level above, and so on
  if Length(Parts) >= 2 then
    FileName := Parts[Length(Parts) - 2] + ' - ' + Parts[Length(Parts) - 1] + ' - ' + FileName;
end.

Note: TWideStringArray is 0-based, so the last element is at index Length(Parts) - 1.

Case conversion

ReNamer provides several functions for Unicode-aware case conversion, all accepting and returning WideString.

To convert the entire filename to uppercase:

begin
  FileName := WideUpperCase(FileName);
end.

To convert only the base name while preserving the extension:

var
  BaseName, Ext: WideString;
begin
  BaseName := WideExtractBaseName(FileName);
  Ext := WideExtractFileExt(FileName);
  FileName := WideUpperCase(BaseName) + Ext;
end.

The same pattern applies to all case functions. Given the input "the quick BROWN fox":

Function Result
WideUpperCase THE QUICK BROWN FOX
WideLowerCase the quick brown fox
WideCaseCapitalize The Quick Brown Fox
WideCaseSentence The quick brown fox
WideCaseInvert THE QUICK brown FOX

Partial case change

To apply case changes to specific characters only, iterate character by character using WideCharUpper and WideCharLower. The built-in case functions operate on the entire string; this approach is needed when the logic depends on position or neighbouring characters.

The example below uppercases the first letter of each word while lowercasing all other letters, but deliberately leaves digits and punctuation untouched:

var
  BaseName, Ext, NewName: WideString;
  I: Integer;
  AtWordStart: Boolean;
begin
  BaseName := WideExtractBaseName(FileName);
  Ext := WideExtractFileExt(FileName);
  NewName := '';
  AtWordStart := True;

  for I := 1 to WideLength(BaseName) do
  begin
    if IsWideCharSpace(BaseName[I]) or IsWideCharPunct(BaseName[I]) then
    begin
      NewName := NewName + BaseName[I];
      AtWordStart := True;
    end
    else if IsWideCharAlpha(BaseName[I]) then
    begin
      if AtWordStart then
        NewName := NewName + WideCharUpper(BaseName[I])
      else
        NewName := NewName + WideCharLower(BaseName[I]);
      AtWordStart := False;
    end
    else
    begin
      NewName := NewName + BaseName[I];  // digits and other chars pass through unchanged
      AtWordStart := False;
    end;
  end;

  FileName := NewName + Ext;
end.

Splitting and rearranging parts

WideSplitString and WideJoinStrings are the primary tools for decomposing a filename around a delimiter and reassembling it in a different order.

Consider files named "Artist - Title.mp3" where the goal is to swap artist and title to produce "Title - Artist.mp3":

var
  Parts: TWideStringArray;
  BaseName, Ext: WideString;
begin
  BaseName := WideExtractBaseName(FileName);
  Ext := WideExtractFileExt(FileName);

  Parts := WideSplitString(BaseName, ' - ');

  if Length(Parts) = 2 then
    FileName := WideTrim(Parts[1]) + ' - ' + WideTrim(Parts[0]) + Ext;
end.

The delimiter passed to WideSplitString can be a multi-character string. WideTrim removes any stray whitespace from each part. The Length(Parts) = 2 guard ensures the script acts only when the expected structure is present.

WideJoinStrings reassembles an array with a delimiter inserted between each item:

var
  Parts: TWideStringArray;
  BaseName, Ext: WideString;
begin
  BaseName := WideExtractBaseName(FileName);
  Ext := WideExtractFileExt(FileName);

  Parts := WideSplitString(BaseName, '_');
  FileName := WideJoinStrings(Parts, ' ') + Ext;
end.

This replaces all underscores in the base name with spaces.

String manipulation

For targeted operations that do not split the whole name, use WidePos, WideCopy, WideInsert, WideDelete, and WideReplaceStr.

Finding and replacing text

To replace all occurrences of a substring (case-sensitive):

begin
  FileName := WideReplaceStr(FileName, '_', ' ');
end.

For case-insensitive replacement, use WideReplaceText instead.

Moving a portion of the filename

To move the first N characters from the front of the base name to the end — for example, relocating a leading year "2024 Concert Recording""Concert Recording 2024":

var
  BaseName, Ext, Prefix: WideString;
begin
  BaseName := WideExtractBaseName(FileName);
  Ext := WideExtractFileExt(FileName);

  Prefix := WideCopy(BaseName, 1, 5);   // "2024 "
  WideDelete(BaseName, 1, 5);
  FileName := WideTrim(BaseName) + ' ' + WideTrim(Prefix) + Ext;
end.

WideCopy extracts a substring by start position and character count. WideDelete removes characters from the string in place. Both use 1-based indexing.

Inserting text at a specific position

WideInsert modifies the target string in place, inserting text at the given 1-based position. A practical use is inserting a separator after a numeric prefix — for example, turning "007Skyfall" into "007 - Skyfall":

var
  BaseName, Ext: WideString;
  I: Integer;
begin
  BaseName := WideExtractBaseName(FileName);
  Ext := WideExtractFileExt(FileName);

  // Find where the leading digits end
  I := 1;
  while (I <= WideLength(BaseName)) and IsWideCharDigit(BaseName[I]) do
    Inc(I);

  // Insert separator only if digits were found and something follows
  if (I > 1) and (I <= WideLength(BaseName)) then
    WideInsert(' - ', BaseName, I);

  FileName := BaseName + Ext;
end.

Separating CamelCase words

This example inserts a space before each uppercase letter that follows a lowercase letter, turning CamelCase names like "TheEverlyBrothers" into "The Everly Brothers":

var
  BaseName, Ext, NewName: WideString;
  I: Integer;
begin
  BaseName := WideExtractBaseName(FileName);
  Ext := WideExtractFileExt(FileName);
  NewName := '';

  for I := 1 to WideLength(BaseName) do
  begin
    if (I > 1) and IsWideCharUpper(BaseName[I]) and IsWideCharLower(BaseName[I - 1]) then
      NewName := NewName + ' ';
    NewName := NewName + BaseName[I];
  end;

  FileName := NewName + Ext;
end.

The condition inserts a space only when the current character is uppercase and the preceding character is lowercase, targeting genuine CamelCase boundaries.

Initializing variables

Script-level variables persist across file iterations within a single preview run. This is intentional — it makes counters and accumulators possible — but it means any variable that should start fresh with each preview must be explicitly reset.

var
  Initialized: Boolean;
  Counter: Integer;
  Prefix: WideString;
begin
  if not Initialized then
  begin
    Counter := 0;
    Prefix := 'Item';
    Initialized := True;
  end;

  Inc(Counter);
  FileName := Prefix + ' ' + IntToStr(Counter) + ' - ' + FileName;
end.

An alternative sometimes seen is checking GetCurrentFileIndex = 1 (or GetCurrentMarkedFileIndex = 1). However, this is unreliable: if the first file in the list is unmarked (excluded from renaming), the script skips it and the index for the first executed file will be greater than 1, so the initialisation block never runs. The Initialized flag avoids this problem entirely and is generally preferred.

Script-level variables are reset at the start of every Preview operation — the script is compiled fresh each time Preview runs. They do not persist between previews. For data that must outlast a single preview, see the Persisting data section.

Serializing files

To add an incrementing index to each filename, use GetCurrentMarkedFileIndex combined with FormatFloat for zero-padded output:

begin
  FileName := FormatFloat('000', GetCurrentMarkedFileIndex) + ' - ' + FileName;
end.

GetCurrentMarkedFileIndex counts only the files actually being processed (marked for renaming), so the index always reflects the position in the renamed set rather than the full file list. FormatFloat('000', ...) pads the number to at least three digits (e.g. 001, 012, 123). Adjust the number of zeroes to control the minimum width.

To start from a custom index (e.g. 100 rather than 1) and use Roman numerals instead of decimal numbers:

var
  Index: Integer;
begin
  Index := GetCurrentMarkedFileIndex + 99;  // starts from 100
  FileName := IntToRoman(Index) + ' - ' + FileName;
end.

Generating random names

Generating a random name can be useful for anonymising files, creating unique temporary identifiers, or testing.

The RandomString function builds a string of a given length by picking characters at random from a supplied alphabet. RandomRange(6, 13) produces random lengths between 6 and 12 characters. Randomize seeds the random number generator and should be called only once per session.

const
  Alphabet = 'abcdefghijklmnopqrstuvwxyz0123456789';

var
  Initialized: Boolean;
begin
  if not Initialized then
  begin
    Randomize;
    Initialized := True;
  end;

  FileName := RandomString(RandomRange(6, 13), Alphabet)
              + WideExtractFileExt(FileName);
end.

Indexing per folder

When files from multiple folders are loaded, you may want the index to reset for each folder. The script below tracks the active folder and resets a local counter whenever it changes:

var
  Initialized: Boolean;
  CurrentFolder: WideString;
  FolderIndex: Integer;
begin
  if not Initialized then
  begin
    CurrentFolder := '';
    FolderIndex := 0;
    Initialized := True;
  end;

  if WideExtractFileDir(FilePath) <> CurrentFolder then
  begin
    CurrentFolder := WideExtractFileDir(FilePath);
    FolderIndex := 0;
  end;

  Inc(FolderIndex);
  FileName := FormatFloat('00', FolderIndex) + ' - ' + FileName;
end.

The Initialized block resets the tracking variables at the start of each preview, ensuring the counter restarts correctly every time.

Serializing duplicates

When multiple files would otherwise produce the same new name, a counter suffix can disambiguate them, producing "Name.ext", "Name (2).ext", "Name (3).ext", and so on.

The script maintains an array of names already assigned in the current preview run and checks each candidate against it:

var
  Initialized: Boolean;
  SeenNames: TWideStringArray;
  BaseName, Ext, Candidate: WideString;
  Counter, I: Integer;
  Found: Boolean;
begin
  if not Initialized then
  begin
    SetLength(SeenNames, 0);
    Initialized := True;
  end;

  BaseName := WideExtractBaseName(FileName);
  Ext := WideExtractFileExt(FileName);
  Candidate := FileName;
  Counter := 1;

  repeat
    Found := False;
    for I := 0 to Length(SeenNames) - 1 do
    begin
      if WideSameText(SeenNames[I], Candidate) then
      begin
        Found := True;
        Break;
      end;
    end;

    if Found then
    begin
      Inc(Counter);
      Candidate := BaseName + ' (' + IntToStr(Counter) + ')' + Ext;
    end;
  until not Found;

  WideAppendStringArray(SeenNames, Candidate);
  FileName := Candidate;
end.

WideSameText performs a case-insensitive comparison, which is appropriate since Windows filenames are case-insensitive. WideAppendStringArray adds the finalised name to the list before moving on to the next file.

Controlling script flow

Use Exit to skip renaming for the current file without an error — FileName is left unchanged and the file passes through untouched:

begin
  if WideExtractFileExt(FileName) = '.tmp' then
    Exit;

  FileName := 'Processed_' + FileName;
end.

To stop processing all subsequent files once a condition is met, use a persistent boolean flag together with Exit. The flag persists across iterations within the same preview run:

var
  StopProcessing: Boolean;
begin
  if StopProcessing then
    Exit;

  if WideDialogYesNo('Rename "' + FileName + '"?') then
    FileName := 'Processed_' + FileName
  else
    StopProcessing := True;
end.

The first time the user clicks No, StopProcessing is set to True and Exit is called for every subsequent file, leaving them all unchanged.

Renaming by file date and time

To rename a file using its last-modified timestamp, read it with FileTimeModified and format it with FormatDateTime:

var
  FileTime: TDateTime;
begin
  FileTime := FileTimeModified(FilePath);
  FileName := FormatDateTime('yyyy-mm-dd hh-nn-ss', FileTime) + WideExtractFileExt(FileName);
end.

FormatDateTime accepts a format string following the Date and Time format conventions. Note that minutes use nn to distinguish them from months (mm). The example above produces names like 2024-06-15 14-32-07.jpg.

To use the creation time instead, replace FileTimeModified with FileTimeCreated.

For scripts that need to adjust a date embedded in the filename, parse it with TryScanDateTime, modify it with one of the Inc* functions, then reformat:

var
  FileDate: TDateTime;
  BaseName, Ext: WideString;
begin
  BaseName := WideExtractBaseName(FileName);
  Ext := WideExtractFileExt(FileName);

  if TryScanDateTime('yyyy-mm-dd hh-nn-ss', BaseName, FileDate) then
  begin
    FileDate := IncHour(FileDate, 2);
    FileName := FormatDateTime('yyyy-mm-dd hh-nn-ss', FileDate) + Ext;
  end;
end.

TryScanDateTime returns False rather than raising an error if the string does not match the pattern, making it safe to use without additional guards.

Adjusting dates embedded in filenames

Cameras and recording devices sometimes embed timestamps in filenames (e.g. 2024-06-15 14-32-07.jpg). When the device clock was set to the wrong timezone, or when DST was not accounted for, every file in a batch needs the same time offset applied.

TryScanDateTime parses the embedded date according to a format pattern, IncHour shifts it by the required amount — handling all day, month, and year rollovers automatically — and FormatDateTime writes it back:

const
  InputFormat  = 'yyyy-mm-dd hh-nn-ss';
  OutputFormat = 'yyyy-mm-dd hh-nn-ss';
  HoursToAdd   = -3;  // use a negative value to subtract

var
  BaseName, Ext: WideString;
  DateTime: TDateTime;
begin
  BaseName := WideExtractBaseName(FileName);
  Ext := WideExtractFileExt(FileName);

  if TryScanDateTime(InputFormat, BaseName, DateTime) then
  begin
    DateTime := IncHour(DateTime, HoursToAdd);
    FileName := FormatDateTime(OutputFormat, DateTime) + Ext;
  end;
end.

TryScanDateTime returns False without raising an error if the filename does not match the expected pattern, so files with non-conforming names are left unchanged.

The input and output format strings are kept as separate constants, making it straightforward to reformat while adjusting — for example, changing OutputFormat to 'yyyy-mm-dd hh.nn.ss' converts the time separators from dashes to dots in the same pass. See the Date and Time format reference for format specifiers.

Reading file content

Scripts can read file content directly, which is useful for assigning names based on data inside the file itself.

Reading a fixed fragment

FileReadFragment reads a specific number of bytes from a given offset (0-based). This is efficient for sampling file headers without loading the entire file:

var
  Header: String;
begin
  Header := FileReadFragment(FilePath, 0, 4);
  if Copy(Header, 1, 2) = 'PK' then
    FileName := '[ZIP] ' + FileName;
end.

Reading the full content

FileReadText returns the entire file as a WideString and automatically detects encoding via byte order mark (BOM), supporting UTF-8 and UTF-16. Files without BOM are treated as ANSI.

A practical use is extracting a value embedded inside the file. The example below renames HTML files using the text content of their <title> tag:

var
  Content, Title: WideString;
  PosStart, PosEnd: Integer;
begin
  if not WideSameText(WideExtractFileExt(FileName), '.html') then
    Exit;

  Content := FileReadText(FilePath);

  PosStart := WideTextPos('<title>', Content);
  PosEnd   := WideTextPos('</title>', Content);

  if (PosStart > 0) and (PosEnd > PosStart) then
  begin
    PosStart := PosStart + 7;  // skip past '<title>'
    Title := WideTrim(WideCopy(Content, PosStart, PosEnd - PosStart));
    if Title <> '' then
      FileName := Title + '.html';
  end;
end.

WideTextPos performs a case-insensitive search, which is appropriate for HTML tags. WideCopy extracts the text between the two tag positions, and WideTrim cleans up any surrounding whitespace.

Renaming from a list

A plain text file with one new name per line can drive batch renaming. Load the list once on first run, then advance a local counter on each subsequent execution:

var
  Initialized: Boolean;
  Names: TWideStringArray;
  Counter: Integer;
begin
  if not Initialized then
  begin
    Names := FileReadTextLines(WideGetEnvironmentVar('USERPROFILE') + '\Desktop\names.txt');
    Counter := 0;
    Initialized := True;
  end;

  if Counter < Length(Names) then
  begin
    FileName := WideTrim(Names[Counter]) + WideExtractFileExt(FileName);
    Inc(Counter);
  end;
end.

FileReadTextLines handles encoding detection automatically. Counter starts at 0 to match the zero-based array index and is incremented after each use. Files beyond the end of the list are left unchanged.

Converting file content encoding

Scripts are not limited to renaming — they can also modify the content of the files being processed. A practical use is bulk-converting text files from the Windows system code page (ANSI) to UTF-8, which is required when moving content to systems or editors that expect UTF-8.

Warning: This script writes directly to the original file on disk during the Preview stage. The change is immediate and irreversible. Always keep backups before running it on a large batch, and verify the result on a single file first.

The script assembles the UTF-8 BOM (EF BB BF) from its hex byte values, checks whether the file already starts with it, and skips the file if so — making the script safe to run more than once on the same set of files:

var
  Content, UTF8BOM: String;

begin
  Content := FileReadContent(FilePath);

  // Define the UTF-8 BOM
  UTF8BOM := Chr($EF) + Chr($BB) + Chr($BF);

  // Skip files that are already UTF-8 encoded (BOM present)
  if Copy(Content, 1, Length(UTF8BOM)) = UTF8BOM then
    Exit;

  // Convert content encoding and add UTF-8 BOM
  Content := UTF8BOM + WinCPToUTF8(Content);

  FileWriteContent(FilePath, Content);
end.

FileReadContent reads the raw bytes of the file as a String. WinCPToUTF8 re-encodes the string from the active Windows system code page to UTF-8. The BOM is then prepended before writing the result back with FileWriteContent, which overwrites the original file.

Note that FileName is not modified in this script. The script's sole effect is on the file content. This is unusual behaviour for a Pascal Script rule and worth keeping in mind when combining it with other rules in the same preset.

Interactive dialogs

Dialog functions pause the preview run and prompt the user, making it possible to inject values or confirm decisions at runtime. Because scripts run for each file in sequence, dialogs should generally be shown only once per preview — use the Initialized flag pattern for this — unless per-file confirmation is specifically intended.

Displaying messages

WideShowMessage is useful during development for inspecting intermediate values:

begin
  WideShowMessage('File: ' + FileName + #13#10 + 'Path: ' + FilePath);
end.

#13#10 is a carriage return and line feed, producing a newline inside the message box.

Yes/No confirmation per file

WideDialogYesNo pauses for user input and returns True if the user clicks Yes:

begin
  if WideDialogYesNo('Apply uppercase to:' + #13#10 + FileName) then
    FileName := WideUpperCase(FileName);
end.

Collecting input once per preview

WideInputQuery shows a text input dialog. It returns True if the user clicked OK, and the entered text is available via the var parameter:

var
  Initialized: Boolean;
  Prefix: WideString;
  OK: Boolean;
begin
  if not Initialized then
  begin
    OK := WideInputQuery('Prefix', 'Enter a prefix to add to all filenames:', Prefix);
    if not OK then
      Exit;
    Initialized := True;
  end;

  FileName := Prefix + FileName;
end.

The dialog appears once, the user types a prefix, and that value is reused for every file in the list.

Regular expressions

The built-in regex functions bring pattern matching directly into scripts, useful for transformations that are too complex for a simple search-and-replace.

Pattern-based replacement

ReplaceRegEx works like the Regular Expressions rule but within a script, allowing the result to be further processed:

begin
  // Collapse any sequence of whitespace into a single space
  FileName := ReplaceRegEx(FileName, '\s+', ' ', False, False);
  FileName := WideTrim(FileName);
end.

With UseSubstitution set to True, backreferences can be used in the replacement pattern. The example below moves a trailing four-digit year to the front:

begin
  // "Concert Recording 2024.mp4" -> "2024 Concert Recording.mp4"
  FileName := ReplaceRegEx(FileName, '^(.*)\s(\d{4})(\.[^.]+)$', '$2 $1$3', True, True);
end.

Extracting matches

MatchesRegEx returns an array of all substrings in the input that match the given pattern:

var
  Matches: TWideStringArray;
  Ext: WideString;
begin
  Ext := WideExtractFileExt(FileName);
  Matches := MatchesRegEx(FileName, '\d+', True);

  if Length(Matches) > 0 then
    FileName := 'Track_' + Matches[0] + Ext;
end.

Applying case conversion to matched groups

SubMatchesRegEx returns the captured groups from the first full match. Combined with MatchesRegEx, it allows case functions to be applied selectively to specific parts of the name:

var
  Matches: TWideStringArray;
  I: Integer;
begin
  // Capitalise each word in the base name
  Matches := MatchesRegEx(WideExtractBaseName(FileName), '[a-zA-Z]+', False);
  for I := 0 to Length(Matches) - 1 do
    FileName := WideReplaceStr(FileName, Matches[I], WideCaseCapitalize(Matches[I]));
end.

Persisting data

Script-level variables reset at the start of every Preview. Two mechanisms are available when data must outlast a single preview run.

Global Variables

Global Variables (added in v7.4.0.2) persist across previews for the entire application session — until manually cleared or the application is closed. They are identified by name and can store any value type.

var
  Initialized: Boolean;
  RunCount: Integer;
begin
  if not Initialized then
  begin
    if HasGlobalVar('RunCount') then
      RunCount := GetGlobalVar('RunCount')
    else
      RunCount := 0;
    Initialized := True;
  end;

  Inc(RunCount);
  SetGlobalVar('RunCount', RunCount);

  FileName := FormatFloat('0000', RunCount) + '_' + FileName;
end.

To reset all global variables at the start of each preview, clear them inside the Initialized block:

begin
  if not Initialized then
  begin
    ClearGlobalVars;
    Initialized := True;
  end;

  // ... rest of script
end.

File-based persistence

For data that must survive application restarts, write it to a file. Using the system temporary folder keeps the file out of the way:

var
  Initialized: Boolean;
  StorageFile: WideString;
  Counter: Integer;
begin
  StorageFile := WideGetTempPath + 'renamer_counter.txt';

  if not Initialized then
  begin
    if WideFileExists(StorageFile) then
      Counter := StrToIntDef(FileReadContent(StorageFile), 0)
    else
      Counter := 0;
    Initialized := True;
  end;

  Inc(Counter);
  FileWriteContent(StorageFile, IntToStr(Counter));

  FileName := FormatFloat('0000', Counter) + '_' + FileName;
end.

Delete the storage file manually (or from a cleanup script) to reset the counter. StrToIntDef returns the default value 0 if the file contains unexpected content, preventing a script error.

Using meta tags

The CalculateMetaTag function extracts metadata from a file using ReNamer's built-in Meta Tags engine, covering EXIF, ID3, document properties, and more. The full list of available tag names is on the Meta Tags reference page.

A common use is renaming photos by their EXIF capture date:

begin
  FileName := CalculateMetaTag(FilePath, 'EXIF_Date') + WideExtractFileExt(FileName);
end.

The date format used here follows the application's default Date and Time format setting. To use a specific format regardless of that setting, use CalculateMetaTagFormat:

begin
  FileName := CalculateMetaTagFormat(FilePath, 'EXIF_Date', 'yyyy-mm-dd') + WideExtractFileExt(FileName);
end.

It is good practice to guard against an empty result, which occurs when the tag is not present in the file:

var
  Tag: WideString;
begin
  Tag := CalculateMetaTag(FilePath, 'ID3_Title');
  if Tag <> '' then
    FileName := Tag + WideExtractFileExt(FileName);
end.

User-defined functions

Scripts can define their own functions and procedures (UDFs), which is useful for encapsulating reusable logic and keeping the main code block readable. Declarations must appear before the main code block.

The example below defines a helper that strips characters forbidden in Windows filenames:

function StripInvalidChars(const S: WideString): WideString;
var
  I: Integer;
  C: WideChar;
begin
  Result := '';
  for I := 1 to WideLength(S) do
  begin
    C := S[I];
    if WidePos(C, '\/:*?"<>|') = 0 then
      Result := Result + C;
  end;
end;

begin
  FileName := StripInvalidChars(FileName);
end.

Note that Result here is the standard Pascal return-value identifier used inside a function body — this is its correct and intended use. User-defined functions and procedures follow standard Pascal syntax; they can accept parameters, call other UDFs, and use all built-in functions.

Importing external functions

Scripts can call functions from any external DLL by declaring them with the external directive followed by an import declaration in one of these formats:

  • function FunctionName(Parameters): ReturnType; external 'FunctionName@Library.dll CallingConvention';
  • procedure FunctionName(Parameters); external 'FunctionName@Library.dll CallingConvention';

A minimal example using a Windows API function:

function GetTickCount: Cardinal;
  external 'GetTickCount@kernel32.dll stdcall';

begin
  FileName := IntToStr(GetTickCount) + '_' + FileName;
end.

The example below retrieves the current Windows username:

function GetUserNameA(lpBuffer: PChar; var nSize: Cardinal): Boolean;
  external 'GetUserNameA@advapi32.dll stdcall';

var
  Size: Cardinal;
  UserName: String;
begin
  Size := 200;
  SetLength(UserName, Size);
  if GetUserNameA(PChar(UserName), Size) then
  begin
    SetLength(UserName, Size - 1); // Size includes trailing null terminator
    FileName := UserName + '_' + FileName;
  end;
end.

The calling convention is stdcall for most Windows API functions and cdecl for many C library functions. An incorrect declaration or a missing DLL will cause a script error. Test new imports carefully.

For integrations with 3rd party command-line tools such as ExifTool or MediaInfo, ExecConsoleApp is often more practical than DLL import — see the Library article for those scripts.

Conditional processing by file type

Scripts process every file in the list, but sometimes logic should apply only to a subset. WideMatchesMask tests a filename against a single wildcard mask; WideMatchesMaskList tests against a semicolon-separated list.

To apply a transformation only to image files:

begin
  if not WideMatchesMaskList(FileName, '*.jpg;*.jpeg;*.png;*.gif;*.webp') then
    Exit;

  FileName := CalculateMetaTag(FilePath, 'EXIF_Date') + WideExtractFileExt(FileName);
end.

The Exit at the top is an efficient way to skip non-matching files early, leaving FileName unchanged. The same pattern applies whenever different rules should apply to different file types within a single script:

begin
  if WideMatchesMask(FileName, '*.mp3') then
    FileName := CalculateMetaTag(FilePath, 'ID3_Title') + '.mp3'
  else if WideMatchesMask(FileName, '*.flac') then
    FileName := CalculateMetaTag(FilePath, 'ID3_Title') + '.flac';
end.

URL-decoding filenames

Files downloaded from the web sometimes retain URL encoding in their names, producing strings like "My%20Document%20%282024%29.pdf". The built-in URLDecode function converts them back to readable form:

var
  BaseName, Ext: WideString;
begin
  BaseName := WideExtractBaseName(FileName);
  Ext := WideExtractFileExt(FileName);
  FileName := URLDecode(BaseName, False) + Ext;
end.

The second parameter, UsePlusAsSpace, controls whether + signs are treated as spaces. Set it to False for standard URL encoding (where spaces are %20), or True for HTML form encoding (where spaces are +).

The extension is decoded separately only when needed, since extensions are typically not URL-encoded.