BernieC has asked for the wisdom of the Perl Monks concerning the following question:

The only previous mention I could find of this was over a decade ago and I hope ancient history. Running 5.32 on win10 and I have the unfortunate situation of having a bunch of files with Unicode in the file names. This, apparently, doesn't bother win10 but I'd like to get rid of the garbage and Perl can't seem to access them, quite. I do an opendir/readdir to get the file name. When I look at the file with windows I see things like
___________________________ ✈️ ✈️ ✈&#6 +5039;
but when I look at the file name in Perl I see
___________________________ ?? ?? ??
and if i try to access the file from Perl I get an "can't find the file" and i even tried doing it via windows with  system "ren \'$UNIFILE\' $reasonablename" and windows can't see the file {and of course trying "-f $UNIFILE" in perl tells me that it isn't there}.

I've always been hopelessly confused in dealing with Unicode in perl but from what I have read, it seems that it is all supposed to "just work"... but apparently not for file names and accessing the file system.

Replies are listed 'Best First'.
Re: Unicode file names
by kcott (Archbishop) on Jan 04, 2023 at 04:48 UTC

    G'day BernieC,

    The character that you show as ️ is "U+FE0F VARIATION SELECTOR-16"; it indicates that the preceding emoji character should be rendered in its graphical form. Its complement is "U+FE0E VARIATION SELECTOR-15"; it indicates that the preceding emoji character should be rendered in its textual form. See Unicode PDF code chart: "Variation Selectors Range: FE00FE0F".

    The character that you show as ✈ is "U+2708 AIRPLANE". As part of the demo code below, I've also used "U+2709 ENVELOPE". Find both of those in Unicode PDF code chart: "Dingbats Range: 270027BF".

    The following two short scripts: create some files with and without Unicode characters in their filenames; identify the filenames with Unicode characters and rename them.

    First, create the files for the demo.

    C:\Users\ken\tmp\pm_11149351_unicode_filenames>dir Volume in drive C is Primary Drive Volume Serial Number is 5A0C-01CD Directory of C:\Users\ken\tmp\pm_11149351_unicode_filenames 04-Jan-23 15:06 <DIR> . 04-Jan-23 15:06 <DIR> .. 04-Jan-23 14:18 337 mkfiles.pl 04-Jan-23 15:03 271 mvfiles.pl 2 File(s) 608 bytes 2 Dir(s) 1,533,002,100,736 bytes free C:\Users\ken\tmp\pm_11149351_unicode_filenames>more mkfiles.pl #!perl use strict; use warnings; use autodie; my $emoji_airplane = "\x{2708}\x{FE0F}"; my $emoji_envelope = "\x{2709}\x{FE0F}"; my @fnames = ( 'AIR_2708_FE0F', "___ $emoji_airplane $emoji_airplane", 'ENV_2709_FE0F', "___ $emoji_envelope $emoji_envelope", ); for my $fname (@fnames) { open my $fh, '>', $fname; } C:\Users\ken\tmp\pm_11149351_unicode_filenames>perl mkfiles.pl

    Use <pre> block to show Unicode characters:

    C:\Users\ken\tmp\pm_11149351_unicode_filenames>dir
     Volume in drive C is Primary Drive
     Volume Serial Number is 5A0C-01CD
    
     Directory of C:\Users\ken\tmp\pm_11149351_unicode_filenames
    
    04-Jan-23  15:32    <DIR>          .
    04-Jan-23  15:32    <DIR>          ..
    04-Jan-23  15:32                 0 AIR_2708_FE0F
    04-Jan-23  15:32                 0 ENV_2709_FE0F
    04-Jan-23  14:18               337 mkfiles.pl
    04-Jan-23  15:03               271 mvfiles.pl
    04-Jan-23  15:32                 0 ___ ✈️ ✈️
    04-Jan-23  15:32                 0 ___ ✉️ ✉️
                   6 File(s)            608 bytes
                   2 Dir(s)  1,533,000,298,496 bytes free
    
    C:\Users\ken\tmp\pm_11149351_unicode_filenames>
    

    Now rename the filenames with Unicode characters.

    C:\Users\ken\tmp\pm_11149351_unicode_filenames>more mvfiles.pl #!perl use strict; use warnings; use autodie; use File::Copy 'move'; opendir(my $dh, '.'); for my $fname (readdir $dh) { next if $fname =~ /^[\x00-\x7f]+$/; (my $new_name = $fname) =~ s/([^\x00-\x7f])/'+U' . ord($1) . 'U+'/ +eg; move($fname, $new_name); } C:\Users\ken\tmp\pm_11149351_unicode_filenames>perl mvfiles.pl C:\Users\ken\tmp\pm_11149351_unicode_filenames>dir Volume in drive C is Primary Drive Volume Serial Number is 5A0C-01CD Directory of C:\Users\ken\tmp\pm_11149351_unicode_filenames 04-Jan-23 15:35 <DIR> . 04-Jan-23 15:35 <DIR> .. 04-Jan-23 15:32 0 AIR_2708_FE0F 04-Jan-23 15:32 0 ENV_2709_FE0F 04-Jan-23 14:18 337 mkfiles.pl 04-Jan-23 15:03 271 mvfiles.pl 04-Jan-23 15:32 0 ___ +U226U++U156U++U136U++U239U++U1 +84U++U143U+ +U226U++U156U++U136U++U239U++U184U++U143U+ 04-Jan-23 15:32 0 ___ +U226U++U156U++U137U++U239U++U1 +84U++U143U+ +U226U++U156U++U137U++U239U++U184U++U143U+ 6 File(s) 608 bytes 2 Dir(s) 1,532,999,979,008 bytes free

    So, that's very much skeleton code to demonstate a technique. You may want to offer an option to type a new filename; you may want something other than the default character conversion to "+U...U+". Perhaps you need to perform this recursively through a directory hierarchy. Depending on how you alter this to suit your preferences, validation, exception handling, and similar checks may be appropriate.

    The ball's in your court. Take it from here ...

    — Ken

      Looks good.. thanks!
Re: Unicode file names
by swl (Prior) on Jan 04, 2023 at 11:33 UTC
      I shoud've guessed that there'd be a module for that kind of thing :o). OTOH, I'd have never guessed its name. Thanks!
Re: Unicode file names
by harangzsolt33 (Deacon) on Jan 04, 2023 at 02:41 UTC
    I wrote this. It gets the thing done using a mixture of JavaScript and Perl. It's simple, fast, and flexible.

    2023-01-04 Edit: I updated the WinReadDir() function. It can now look into directories that have special characters in their name.
    2023-01-07: Edit: Added character filters to prevent code injection vulnerability with $REGEX, and I also shortened the $JSCODE somewhat. Removed an unused variable.

    #!/usr/bin/perl use 5.004; use strict; use warnings; my $PATH = GetCurrentDirectory(); my @L = WinReadDir($PATH, 0, 'S|T|M|N', 40); print "\n$PATH\n"; print "\nFile(s) in the current directory:\n"; foreach(@L) { my ($Size, $Attr, $Date, $Name) = split(/\|/, $_); print "\n $Size ", TimeStamp($Date), ' ', GetStrAttr($Attr), " $N +ame"; if (index($Name, '{') >= 0) { print "\n\nType a new file name for this file (without path).", "\nor just press ENTER to skip renaming.\n"; my $NewName = <STDIN>; if (length($NewName)) { my $FullName = $PATH . "\\" . $Name; print "RENAME:", WinRenameFile($FullName, $NewName); } } } print "\n"; exit; ################################################## # File | v2023.1.4 # This function converts a Windows file attribute # number to string representation. # # Usage: STRING = GetAttrStr(ATTRIBUTE) # sub GetStrAttr { my $A = defined $_[0] ? $_[0] : 0; my $OUTPUT = '.' x 8; my $LETTER = 'LCASHRDV'; my $P = 0; foreach(1024, 2048, 32, 4, 2, 1, 16, 8) { if ($A & $_) { vec($OUTPUT, $P, 8) = vec($LETTER, $P, 8); } $P++; } while (length($OUTPUT) < 4) { $OUTPUT .= '.'; } return $OUTPUT; } ################################################## # File | v2023.1.4 # This function is a customizable readdir() function # for Windows. It returns the directory contents as # an array using a tiny JavaScript program to # collect the data. [Tested with TinyPerl 5.8 on WinXP.] # # Usage: LIST = WinReadDir(PATH, [SUBDIR, [PATTRN, [MAX, [REGEX]]]]) # # NOTE: This function only works in Windows XP or higher! # In Linux and other operating systems, an # empty list will be returned. # # The easiest way to use the function is to simply # provide a path as the first argument: # # my @LIST = WinReadDir('C:\\HOME\\PERL'); # # This will return something like this: # # 00000000000 1672793716 More\ # 00000000002 1656954558 Myfile{1758}.txt # 00000000241 1574651186 SPEAK.VBS # 00000001663 1607229792 Numbers.pl # 00000002456 1670070972 cut.pl # 00000059994 1672797060 filelib.pl # 00000361490 1669902950 lib.pl # # The first 12 digits are the file's size. The next 10 digits # are the file's last modified date given in seconds. # If a file name ends with backslash "\" character, then # that means it's a directory name. # # Each line is a separate array element. # If a name contains unicode characters, the special # characters will appear as a number between brackets. # For example: Myfile{1758}.txt # If you press ALT + 1758, it produces a little star icon. # And that's the code that appears in the file name there. # # If a file or directory name contains the '{' character, # then it will appear as '{123}' Also, if you need to refer # to a directory that contains the '{' then again, # you would do this: # # my @LIST = WinReadDir('C:\\TEMP\\x{123}45}'); # # As a result, the function will look in the directory C:\TEMP\x{45} # # The PATH may also contain special characters: # # my @LIST = WinReadDir('C:\\TEMP\\MyWeirdFolder{9700}\\data', 1); # # The second argument is either 1 or 0. Default is zero. # One means that sub-directories will be scanned as well. # In that case, the output will look something like this: # # 00000000000 1661404204 IMGS\ # 00000000000 1659249130 DOCS\ # 00000014010 1642132972 DOCS\0023.bmp # 00000014060 1115495874 DOCS\index.html # 00000015838 1667767236 rr.bmp # 00000015866 1667184404 MANDEL3.BMP # 00000016730 1141128000 FeatherTexture.bmp # 00000017062 1141128000 IMGS\Coffee Bean.bmp # 00000018938 1666659612 TEMP.jif # # Notice that directories' size is always zero. # Also notice that the list is sorted, and whatever comes # first will determine how the list is sorted. You can # change this order using the 3rd argument: # # my @LIST = WinReadDir('C:\\TEMP', 1, 'N S M'); # # The string 'N S M' will substitute the Name of the file # first, then the Size and finally the Modified date all # separated by spaces. You may use a different separator. # I use space just because it makes it easier to read, but # you could use the '|' character which then would allow # you to split the items using the split() function: # # my @LIST = WinReadDir('C:\\TEMP', 1, 'N|S|M'); # foreach (@LIST) # { # my @ITEM = split(/\|/, $_); # # $ITEM[0] ---> NAME OF FILE # # $ITEM[1] ---> FILE SIZE # # $ITEM[2] ---> MODIFIED DATE # } # # There are more values available. For example, if you want # the full name of the file with path, then use letter 'F': # # my @LIST = WinReadDir('C:\\TEMP', 1, 'S**F'); # # This will produce a list which starts with the file size, # followed by the long file name. It will look something like this: # # 000000000000**C:\TEMP\MyWeirdFolder{931}{931}\ # 000000000000**C:\TEMP\x{123}45}\ # 000000000000**C:\TEMP\x{123}45}\test.txt # 000000000002**C:\TEMP\testing{931}.txt # # These are more values that you can use: # # S = insert file size # N = insert file name # M = insert file last modified date # C = insert file date of creation # A = insert file last accessed date # H = insert file's short name (8+3 format) # F = insert file's full name with path # T = insert file's attributes # # You can create your own customized list using a # combinations of the above letters. # # The 4th argument allows you to limit the directory listing # to only X items. For example, here we request only the # first 10 files in the directory list. And we want the file # attribute first, then date of creation, and the full name: # # my @L = WinReadDir("C:\\WINDOWS", 0, 'T|C|F', 10); # # Returns the following list: # # 0016|1665934687|C:\WINDOWS\Config\ # 0016|1665934687|C:\WINDOWS\Cursors\ # 0016|1665934687|C:\WINDOWS\Help\ # 0016|1665934687|C:\WINDOWS\Media\ # 0016|1665934687|C:\WINDOWS\msagent\ # 0016|1665934687|C:\WINDOWS\repair\ # 0016|1665934687|C:\WINDOWS\system32\ # 0016|1665934687|C:\WINDOWS\system\ # 0018|1665934687|C:\WINDOWS\inf\ # 0021|1665934687|C:\WINDOWS\Fonts\ # # The attribute is a 12-bit integer. # The meaning of the bits is described here: # # 0 = Normal file # 1 = Read-only file # 2 = Hidden file # 4 = System file # 8 = Disk drive volume label (Not a real file) # 16 = Directory # 32 = Archive (most files) # 1024 = Link or shortcut # 2048 = Compressed file # # The 5th argument allows you to filter the results using a # regex enclosed as a string. Now, keep in mind, we are not # using Perl's regex engine. This is nowhere near as # sophisticated, but it's better than nothing. Here, for # example, we search for all executable files in the # Windows directory: # # my @L = WinReadDir("C:\\WINDOWS", 0, 'S bytes, name: F', 10, '/ex +e/i'); # # 000000010752 bytes, name: C:\WINDOWS\hh.exe # 000000015360 bytes, name: C:\WINDOWS\TASKMAN.EXE # 000000025600 bytes, name: C:\WINDOWS\twunk_32.exe # 000000049680 bytes, name: C:\WINDOWS\twunk_16.exe # 000000069120 bytes, name: C:\WINDOWS\NOTEPAD.EXE # 000000069632 bytes, name: C:\WINDOWS\ALCMTR.EXE # 000000086016 bytes, name: C:\WINDOWS\SOUNDMAN.EXE # 000000146432 bytes, name: C:\WINDOWS\regedit.exe # 000000256192 bytes, name: C:\WINDOWS\winhelp.exe # 000000283648 bytes, name: C:\WINDOWS\winhlp32.exe # # In the next example, we want to find all files that have # some special characters in their file name: # # my @L = WinReadDir("C:\\TEMP", 0, 'S M T N', 0, '/[{]+/i'); # # So, we get this list: # # 000000000000 1672896164 0016 MyWeirdFolder{931}{931}\ # 000000000000 1672896244 0032 testing{931}.txt # 000000000000 1672896984 0016 x{123}45}\ # # Usage: LIST = WinReadDir(PATH, [SUBDIR, [PATTRN, [MAX, [REGEX]]]]) # sub WinReadDir { my @DIR; $^O =~ m/MSWIN/i or return @DIR; my $PATH = defined $_[0] ? $_[0] : 'C:\\'; $PATH =~ tr|\/|\\|; # Convert / to \ $PATH =~ tr|\\||s; # Remove duplicate backslash. $PATH =~ s/\\/\\\\/g; # Now double each backslash. my $RET = defined $_[2] ? $_[2] : ''; length($RET) or $RET = 'S M N'; $RET =~ tr|'\r\n\\||d; # Filter out unsafe characters my $START = -1; # Start of separator string my $J = ''; # JavaScript code will go here my @f = ('toASCII(n.slice(PATHLEN))+d', 'fSize(s)', 'toASCII(n)+d', 'fDate(f.DateCreated)', 'fDate(f.DateLastModified)', 'fDate(f.DateLastAccessed)', 'fAttr(f)', 'f.ShortName'); for (my $i = 0; $i < length($RET); $i++) { my $c = index('NSFCMATH', substr($RET, $i, 1)); if ($c >= 0) { if ($START >= 0) { $J .= "'" . substr($RET, $START, $i - $START) . "',"; } $J .= "$f[$c],"; $START = -1; } elsif ($START < 0) { $START = $i; } } if ($START < 0) { $J = substr($J, 0, length($J) - 1); } else { $J .= "'" . substr($RET, $START) . "'"; } undef $RET; # Okay, at this point, $J should contain a list of properties # we want to save from each directory and file. These are things # we just plucked out of @f. For example, to record the file size, # $RET had to include the letter 'S' and when we see the letter S, # we insert "fSize(s)," into $J. This list in $J will then become # part of the JavaScript code. When the JS script runs, it creates # a list, joins the items and pushes the string into an array. my $RECURSIVE = defined $_[1] && $_[1] ? 'DIR(FullName);' : ''; my $MAX = defined $_[3] ? $_[3] : 0; $MAX =~ tr|0-9||cd; # Remove everything except numbers $MAX = ($MAX) ? "if(OUTPUT.length>=$MAX)return;" : ''; my $REGEX = defined $_[4] ? $_[4] : ''; # If the regex match is not true, then we continue reading # the directory, otherwise we add the file to our list. # The Regex only tests the name of the file, not its path. # So, if the path contains the pattern we're looking for, # we won't see that. # If $REGEX is not provided, then it won't become part of the code. if (length($REGEX)) { # We need to remove forward slashes and backslashes # among other things to prevent code injection vulnerability: $REGEX =~ tr|\/\\'"<>\r\n||d; $REGEX = "NameOnly=toASCII(FullName+'').split(BS).pop();if(!(/$REG +EX/.test(NameOnly)))continue;"; } my $JSCODE = "PATH=CNV('$PATH');OUTPUT=[];BS='\\\\';PATHLEN=PATH.len +gth+((PATH.slice(-1)==BS)?0:1);try{FSO=new ActiveXObject('Scripting.F +ileSystemObject');DIR(PATH);WScript.StdOut.WriteLine(OUTPUT.sort().jo +in('\\n'));}catch(e){}function PACK(d,n){$MAX var f=d?FSO.GetFolder(n +):FSO.GetFile(n);var s=d?0:f.Size;n+='';OUTPUT.push([$J].join(''));}f +unction CNV(s){var i,P;s=s.split('{');for(i=0;i<s.length;i++){P=s[i]. +indexOf('}');if(P>0)s[i]=String.fromCharCode(s[i].substr(0,P)&0xffff) ++s[i].slice(P+1);}return s.join('');}function DIR(p){var F=FSO.GetFol +der(p),FC,File,FullName;for(FC=new Enumerator(F.SubFolders);!FC.atEnd +();FC.moveNext()){FullName=FC.item();Folder=FSO.GetFolder(FullName);$ +REGEX PACK(BS,FullName);$RECURSIVE}for(FC=new Enumerator(F.files);!FC +.atEnd();FC.moveNext()){FullName=FC.item();$REGEX PACK('',FullName);} +}function toASCII(s){var i,T=[];s+='';for(i=0;i<s.length;i++){c=s.cha +rCodeAt(i);T.push(((c<32&&c!=10&&c!=13)||c>126||c==123)?'{'+c+'}':s.c +harAt(i));}return T.join('');}function fSize(s){return('000000000000' ++s).slice(-12);}function fDate(d){return('0000000000'+(d*1)).slice(-1 +3).substr(0,10);}function fAttr(f){return('0000'+f.Attributes).slice( +-4);}"; mkdir "C:\\TEMP"; my $JSFILE = "C:\\TEMP\\GETDIR.JS"; open(my $FILE, ">$JSFILE") or return @DIR; binmode $FILE; print $FILE $JSCODE; close $FILE; if (-s $JSFILE != length($JSCODE)) { return @DIR; } @DIR = split(/\n/, `CSCRIPT.EXE //NOLOGO $JSFILE`); unlink $JSFILE; return @DIR; } ################################################## # File | v2023.1.3 # This function renames a file whose name contains # special unicode characters. It cannot rename # directories, only files! # # Usage: STATUS = WinRenameFile(FULLPATH, NEWNAME, [FORCE]) # # Unicode characters must be placed # between {} brackets in decimal format. # For example: {9674} is the representation of a # little diamond shaped character that you can # replicate by pressing ALT + 9674. # # So, if you have a file called "Myfile{9674}.txt" # and you want to rename it to "Myfile.txt" then simply do: # # WinRenameFile('C:\\HOME\\Myfile{9674}.txt', 'Myfile.txt'); # # This will rename the file. If you want to make sure that # the file gets renamed even if there is another file by that name, # then use 1 for the third argument: # # WinRenameFile('C:\\Users\\Zsolt\\Desktop\\Myfile{9674}.txt', 'Myfi +le.txt', 1); # # And if the new file name exists AND happens to be read-only, # the file will not be renamed. However, if you specify 2 for the # third argument, then the read-only "Myfile.txt" will be deleted # first, and then the file will be renamed anyway: # # WinRenameFile('C:\\HOME\\Myfile{9674}.txt', 'Myfile.txt', 2); # # Note: Using 1 or 2 option will not remove "Myfile.txt" if that # happens to be a directory! # # This can be used to remove unicode letters to make # files accessible to simple command-line applications. # # You may use normal forward slash in place of backslash. # It makes things a bit clearer: # # WinRenameFile('C:/HOME/Myfile{9674}.txt', 'Myfile.txt'); # # You must not type any slashes in the second name. # The new name must only contain a file name and extension. # If you want to move the file to another directory or another # drive, you should use the builtin rename() function. # # This function returns non-zero on success or # zero if the file could not be renamed. # # NOTE: This function only works in Windows XP or higher! # In Linux and other operating systems, no change will # take place and the function always returns zero. # # Usage: STATUS = WinRenameFile(FULLPATH, NEWNAME, [FORCE]) # sub WinRenameFile { $^O =~ m/MSWIN/i or return 0; defined $_[0] && defined $_[1] or return 0; my ($OLD, $NEW) = @_; my $FORCE = defined $_[2] ? $_[2] : 0; $OLD =~ tr|\x00-\x1F\"$\|<>||d; # Remove illegal characters $OLD =~ tr|\/|\\|; # Convert / to \ $OLD =~ tr|\\||s; # Remove duplicate backslash. $OLD =~ s/\\/\\\\/g; # Now double each backslash. $NEW =~ tr|\x00-\x1F\"$\|<>||d; # Remove illegal characters $NEW =~ tr|\\|\/|; # Convert \ to / if (index($NEW, '/') >= 0) { return 0; } length($OLD) or return 0; length($NEW) or return 0; my $JSCODE = "FORCE=$FORCE;OLD=CNV('$OLD');NEW=CNV('$NEW');try{FSO=n +ew ActiveXObject('Scripting.FileSystemObject');if(!FSO.FileExists(OLD +)){BYE(0);}if(FORCE){FULL=NEW;if(NEW.indexOf('\\\\')<0){P=OLD.lastInd +exOf('\\\\');if(P>=0)FULL=OLD.substr(0,P+1)+NEW;}if(FORCE==2)FSO.Dele +teFile(FULL,1);else FSO.DeleteFile(FULL);}}catch(e){}try{F=FSO.GetFil +e(OLD);F.name=NEW;BYE(1);}catch(e){BYE(0);}function BYE(x){WScript.Qu +it(x);}function CNV(s){var i,P;s=s.split('{');for(i=0;i<s.length;i++) +{P=s[i].indexOf('}');if(P>0)s[i]=String.fromCharCode(s[i].substr(0,P) +&0xffff)+s[i].slice(P+1);}return s.join('');}"; mkdir "C:\\TEMP"; my $JSFILE = "C:\\TEMP\\RENAMER.JS"; open(my $FILE, ">$JSFILE") or return 0; binmode $FILE; print $FILE $JSCODE; close $FILE; if (-s $JSFILE != length($JSCODE)) { return 0; } my $STATUS = system("CSCRIPT.EXE //NOLOGO $JSFILE"); unlink $JSFILE; return $STATUS; } ################################################## # File | v2022.7.11 # Returns the current directory. (If a drive letter # is provided in the first argument, then it returns # the current directory for that drive. This only # applies to DOS and Windows where each drive letter # has its own current directory. If no drive letter # is provided, then it returns the current directory # of the current drive under DOS and Windows.) # # Usage: STRING = GetCurrentDirectory([DRIVE]) # sub GetCurrentDirectory { if ($^O =~ /DOS|MSWIN/i) { my $DRV = defined $_[0] ? substr(Trim($_[0]), 0, 2) : ''; return Trim(`CD $DRV`); } return Trim(`pwd`); } ################################################## # Time | v2022.2.11 # This function returns the time given in seconds # (or the current time) as a string # in the following format: # # TimeStamp([TIME]) --> YYYY-MM-DD HH:MM:SS # # In contrast, the builtin function localtime() # returns the date and time in the following format: # localtime() --> Ddd Mmm D HH:MM:SS YYYY # # Usage: STRING = TimeStamp([SECONDS]) # sub TimeStamp { my @D = localtime(defined $_[0] ? $_[0] : time); return sprintf('%.04d-%.02d-%.02d %.02d:%.02d:%.02d', (1900+$D[5]), (1+$D[4]), $D[3], $D[2], $D[1], $D[0]); } ##################################################

      JSHint

      The Crux of the Biscuit is the Apostrophe

        Thanks! That website helped me spot a variable that I declared but never used. Also, I shortened the JS code a little bit after I realized I could move parts of it to the front and eliminate functions that were only used once.