Page 1 of 2
GetExtensionPart fails with a space
Posted: Wed Jul 30, 2025 10:25 am
by BarryG
Just discovered this problem today:
Code: Select all
Debug GetExtensionPart("c:\test\file.john doe") ; Returns "" instead of "john doe"
Yes, extensions don't normally have a space in them, but my app won't work if they do.

Is this a bug?
Re: GetExtensionPart fails with a space
Posted: Wed Jul 30, 2025 10:58 am
by Fred
Windows doesn't support this, you can check in Explorer, the column 'type' will display nothing if you have a space.
Re: GetExtensionPart fails with a space
Posted: Wed Jul 30, 2025 11:14 am
by RASHAD
Hi BarryG
More simple (I think)
Code: Select all
Procedure.s MyGetExtensionPart(txt$)
txt$ = ReplaceString(txt$," ","_")
ext$ = GetExtensionPart(txt$)
ext$ = ReplaceString(ext$,"_"," ")
ProcedureReturn ext$
EndProcedure
Debug MyGetExtensionPart("c:\test\file.john doe") ; Returns "" instead of "john doe"
Re: GetExtensionPart fails with a space
Posted: Wed Jul 30, 2025 11:17 am
by BarryG
Nice, Rashad! I will use a variation of that.

(Can't use underscores in case they're already in the ext).
Re: GetExtensionPart fails with a space
Posted: Wed Jul 30, 2025 11:23 am
by RASHAD
How about
Code: Select all
Procedure.s MyGetExtensionPart(txt$)
txt$ = ReplaceString(txt$," ","*#?")
ext$ = GetExtensionPart(txt$)
ext$ = ReplaceString(ext$,"*#?"," ")
ProcedureReturn ext$
EndProcedure
Debug MyGetExtensionPart("c:\test\file.john doe") ; Returns "" instead of "john doe"
Re: GetExtensionPart fails with a space
Posted: Wed Jul 30, 2025 11:26 am
by BarryG
I ended up using the below, since I assume most file extensions don't have Chr(1) in them.
Note that I use GetExtensionPart() first, as it's probably faster (for my bulk file renaming). I only fall into the ReplaceString() parts if actually needed.
Code: Select all
Procedure.s GetFileExtension(file$) ; https://www.purebasic.fr/english/viewtopic.php?p=643514#p643514
ext$=GetExtensionPart(file$)
If ext$=""
ext$=ReplaceString(file$," ",Chr(1))
ext$=ReplaceString(GetExtensionPart(ext$),Chr(1)," ")
EndIf
ProcedureReturn ext$
EndProcedure
file$="C:\File Path\Name.John Doe"
Debug GetExtensionPart(file$) ; ""
Debug GetFileExtension(file$) ; John Doe
Re: GetExtensionPart fails with a space
Posted: Wed Jul 30, 2025 11:38 am
by NicTheQuick
Here are my ideas. I don't like the ReplaceString method.
Code: Select all
Procedure.s MyGetExtensionPart1(path.s)
path = GetFilePart(path)
If Not FindString(path, "."):
ProcedureReturn ""
EndIf
ProcedureReturn ReverseString(StringField(ReverseString(path), 1, "."))
EndProcedure
Procedure.s MyGetExtensionPart2(text.s)
Protected dotCount.i = CountString(text, ".")
If Not dotCount
ProcedureReturn ""
EndIf
ProcedureReturn StringField(text, dotCount + 1, ".")
EndProcedure
Debug MyGetExtensionPart1("c:\test\file.john doe")
Debug MyGetExtensionPart2("c:\test\file.john doe")
Debug MyGetExtensionPart1("john doe")
Debug MyGetExtensionPart2("john doe")
Btw: Would be nice if StringField() could work with negative indexes.
Re: GetExtensionPart fails with a space
Posted: Wed Jul 30, 2025 2:37 pm
by Little John
NicTheQuick wrote: Wed Jul 30, 2025 11:38 am
Code: Select all
Procedure.s MyGetExtensionPart2(text.s)
Protected dotCount.i = CountString(text, ".")
If Not dotCount
ProcedureReturn ""
EndIf
ProcedureReturn StringField(text, dotCount + 1, ".")
EndProcedure
This is elegant, but not bullet-proof. Try this:
Code: Select all
Debug GetExtensionPart("c:\test\.foo")
Debug MyGetExtensionPart2("c:\test\.foo")
I have extended your code a little bit:
Code: Select all
Procedure.s MyGetExtensionPart2 (path$)
Protected filePart$ = GetFilePart(path$)
Protected dotCount.i = CountString(filePart$, ".")
If dotCount = 0 Or (dotCount = 1 And Asc(filePart$) = '.')
ProcedureReturn ""
Else
ProcedureReturn StringField(filePart$, dotCount+1, ".")
EndIf
EndProcedure
; -- Demo
Debug GetExtensionPart("c:\test.it\demo.txt") ; -> "txt"
Debug MyGetExtensionPart2("c:\test.it\demo.txt") ; -> "txt"
Debug "----------------"
Debug GetExtensionPart("c:\test.it\demo") ; -> ""
Debug MyGetExtensionPart2("c:\test.it\demo") ; -> ""
Debug "----------------"
Debug GetExtensionPart("c:\test.it\.foo") ; -> ""
Debug MyGetExtensionPart2("c:\test.it\.foo") ; -> ""
Debug "----------------"
Debug GetExtensionPart("c:\test.it\file.john doe") ; -> ""
Debug MyGetExtensionPart2("c:\test.it\file.john doe") ; -> "john doe"
Re: GetExtensionPart fails with a space
Posted: Wed Jul 30, 2025 2:40 pm
by Wolfram
Code: Select all
Procedure.s MyGetExtensionPart3(*p)
*str.string = @*p
length = StringByteLength(*str\s)
*char.character = *p +length
Repeat
*char -SizeOf(character)
If *char\c = 46
*char +SizeOf(character)
*str = @*char
ProcedureReturn *str\s
EndIf
Until *char = *str
ProcedureReturn ""
EndProcedure
Debug MyGetExtensionPart3(@"c:\test\file.john doe") ; -> john doe
Re: GetExtensionPart fails with a space
Posted: Wed Jul 30, 2025 4:16 pm
by RASHAD
I like the game
Code: Select all
Procedure.s MyGetExtensionPart(path.s)
pos = FindString(path,".")
extn = Len(path)-pos
ProcedureReturn Right(path,extn)
EndProcedure
Debug MyGetExtensionPart("c:\test\file.john doe")
Re: GetExtensionPart fails with a space
Posted: Wed Jul 30, 2025 6:12 pm
by NicTheQuick
RASHAD wrote: Wed Jul 30, 2025 4:16 pm
I like the game
Code: Select all
Procedure.s MyGetExtensionPart(path.s)
pos = FindString(path,".")
extn = Len(path)-pos
ProcedureReturn Right(path,extn)
EndProcedure
Debug MyGetExtensionPart("c:\test\file.john doe")
It's not that easy. Try paths with multiple dots.
Also you could even simplify it using `Mid()` instead of `Right()`.

Re: GetExtensionPart fails with a space
Posted: Wed Jul 30, 2025 6:39 pm
by mk-soft
Fred wrote: Wed Jul 30, 2025 10:58 am
Windows doesn't support this, you can check in Explorer, the column 'type' will display nothing if you have a space.
I have to agree with Fred. Thus, this is not an extension and thus your functions are an error ...
Re: GetExtensionPart fails with a space
Posted: Wed Jul 30, 2025 6:47 pm
by Little John
mk-soft wrote: Wed Jul 30, 2025 6:39 pm
Fred wrote: Wed Jul 30, 2025 10:58 am
Windows doesn't support this, you can check in Explorer, the column 'type' will display nothing if you have a space.
I have to agree with Fred.
And what about Linux and macOS?
Re: GetExtensionPart fails with a space
Posted: Wed Jul 30, 2025 9:24 pm
by RASHAD
Code: Select all
Procedure.s MyGetExtensionPart(path.s)
For i = 1 To 20
ext.s = Right(path,i)
If Left(ext,1) = "."
ext = Right(ext,i-1)
Break
EndIf
Next
ProcedureReturn ext
EndProcedure
path.s = "c:\test\file.file.file.john doe"
Debug MyGetExtensionPart(path)
Re: GetExtensionPart fails with a space
Posted: Wed Jul 30, 2025 9:41 pm
by SMaag
"c:\test\file.john doe" is not allowed and an error!
But it is possible to get correct "john doe" with FindCharRevers()
Code: Select all
EnableExplicit
Prototype.i FindCharReverse(String$, cSearch.c, *outLength.Integer=0)
Global FindCharReverse.FindCharReverse ; define Prototype-Handler for FindCharReverse
Procedure.i _FindCharReverse(*String.Character, cSearch.c, *outLength.Integer=0)
; ============================================================================
; NAME: FindCharReverse
; DESC: !PointerVersion! use it as ProtoType FindCharReverse()
; DESC: Finds the first psoition of Char from reverse
; DESC:
; VAR(*String.Character) : Pointer to the String
; VAR(cSearch) : Character to find
; VAR(*outLength.Integer): Optional a Pointer to an Int to receive the Length
; RET.i : Character Position 1..n of cSearch or 0 if no Char was found
; ============================================================================
CompilerIf (#PB_Compiler_Backend = #PB_Backend_Asm) And #PB_Compiler_64Bit And #PB_Compiler_Unicode
; Used Registers:
; RAX : Pointer *String
; RCX : operating Register and Bool: 1 if NullChar was found
; RDX : operating Register
; R8 : *pCharFound -> NoOfChars
; R9 : operating Register
; XMM0 : the 4 Chars
; XMM1 : cSearch shuffeled to all Words
; XMM2 : 0 to search for EndOfString
; XMM3 : the 4 Chars Backup
; If you use XMM4..XMM7 you have to backup it first
; XMM4 :
; XMM5 :
; XMM6 :
; XMM7 :
; ----------------------------------------------------------------------
; Check *String-Pointer and MOV it to RAX as operating register
; ----------------------------------------------------------------------
!MOV RAX, [p.p_String] ; load String address
!Test RAX, RAX ; If *String = 0
!JZ .Return ; Exit
!SUB RAX, 8 ; Sub 8 to start with Add 8 in the Loop
; ----------------------------------------------------------------------
; Setup start parameter for registers
; ----------------------------------------------------------------------
; your indiviual setup parameters
!MOVZX RDX, WORD [p.v_cSearch] ; ZeroExpanded load of 1 Word = load Char
!MOVQ XMM1, RDX
!PSHUFLW XMM1, XMM1, 0 ; Shuffle/Copy Word0 to all Words
; here are the standard setup parameters
!XOR RCX, RCX ; operating Register and BOOL for EndOfStringFound
!XOR R8, R8 ; *pCharFound = 0
!PXOR XMM2, XMM2 ; XMM2 = 0 ; Mask to search for NullChar = EndOfString
; ----------------------------------------------------------------------
; Main Loop
; ----------------------------------------------------------------------
!.Loop:
!ADD RAX, 8 ; *String + 8 => NextChars
!MOVQ XMM0, [RAX] ; load 4 Chars to XMM0
!MOVQ XMM3, [RAX] ; load 4 Chars to XMM3
!PCMPEQW XMM0, XMM2 ; Compare with 0
!MOVQ RDX, XMM0 ; RDX CompareResult contains FFFF for each NullChar
!TEST RDX, RDX ; If 0 : No NullChar found
!JZ .EndIf ; JumpIfEqual 0 => JumpToEndif if Not EndOfString
; If EndOfStringFound
; Caclulate the Bytepostion of EndOfString [0..3] using Bitscan
!BSF RDX, RDX ; BitSanForward => No of the LSB
!SHR RDX, 3 ; BitNo to ByteNo
!ADD RAX, RDX ; Actual StringPointer + OffsetOf_NullChar
!MOV RCX, RDX ; Save ByteOfsett of NullChar in RCX
!SUB RAX, [p.p_String] ; RAX *EndOfString - *String
!SHR RAX, 1 ; NoOfBytes to NoOfWord => Len(String)
; ------------------------------------------------------------
; check for Return of Length and and move it to *outLength
; ------------------------------------------------------------
!MOV RDX, [p.p_outLength]
!TEST RDX, RDX
!JZ @f ; If *outLength
!MOV [RDX], RAX ; *outLength = Len()
!@@: ; Endif
; ------------------------------------------------------------
; If a Nullchar was found Then create a Bitmask for setting all Chars after the NullChar to 00h
; In RCX ist the Backup of the ByteOffset of NullChahr
!CMP RCX, 6 ; If NullChar is the last Char : Byte[7,6]=Word[3]
!JGE @f ; => we don't have to eliminate chars from testing
; If WordPos(EndOfString) <> 3 ; Word3 if EndOfString is in Bit 48..63 = Word 3
!SHL RCX, 3 ; ByteNo to BitNo
!NEG RCX ; RCX = -LSB
!ADD RCX, 63 ; RCX = (63-LSB)
!XOR RDX, RDX ; RDX = 0
!BTS RDX, 63 ; set Bit 63 => RDX = 8000000000000000h
!SAR RDX, CL ; Do an arithmetic Shift Right (63-LSB) : EndOfString=Word2 => Mask $FFFF.FFFF.0000.0000, Word1 $FFFF.FFFF.FFFF.0000
!NOT RDX ; Now invert our Mask so we get a Mask to fileter out all Chars after EndOfString $0000.0000.FFFF.FFFF or $0000.0000.0000.FFFF
!MOVQ XMM0, RDX ; Now move this Mask to XMM0, the operating Register
!PAND XMM3, XMM0 ; XMM3 the CharBackup AND Mask => we select only Chars up to EndOfString
!@@:
!MOV RCX, 1 ; BOOL EndOfStringFound = #TRUE
!.EndIf: ; Endif ; EndOfStringFound
; ------------------------------------------------------------
; Start of function individual code! Do not use RCX here!
; ------------------------------------------------------------
; Count number of found Chars
!MOVQ XMM0, XMM3 ; Load the 4 Chars to operating Register
!PCMPEQW XMM0, XMM1 ; Compare the 4 Chars with cSearch
!MOVQ RDX, XMM0 ; CompareResult to RDX
!TEST RDX, RDX
!JZ @f ; Jump to Endif if cSearch not found
; !BSF RDX, RDX ; BitScan finds the LSB Bitposition
; because we need Character from right, we have to use BSR, not BSF
!BSR RDX, RDX ; BitScan finds the MSB Bitposition
!SHR RDX, 3 ; BitNo => ByteNO
!MOV R8, RAX ; R8 = *pCharFound
!ADD R8, RDX ; ADD ByteNo to *String
!@@:
; ------------------------------------------------------------
!TEST RCX, RCX ; Check BOOL EndOfStringFound
!JZ .Loop ; Continue Loop if Not EndOfStringFound
; ----------------------------------------------------------------------
; Handle Return value an POP-Registers
; ----------------------------------------------------------------------
!XOR RAX, RAX ; RAX = 0
!TEST R8, R8 ; TEST *pCharFound = 0
!JZ .Return ; If *pCharFound = 0 Then .Return 0
; Else Return NoOfChars
!MOV RAX, R8 ; RAX = *pCharFound
!SUB RAX, [p.p_String] ; RAX = *pCharFound - *String
!SHR RAX, 1 ; Bytes -> Chars
!INC RAX
!.Return:
ProcedureReturn ; RAX
CompilerElse
; ----------------------------------------------------------------------
; PB-Standard-Code
; ----------------------------------------------------------------------
Protected *pChar.Character = *String
Protected *LastChar
If Not *pChar
ProcedureReturn 0
EndIf
While *pChar\c
If *pChar\c = cSearch
*LastChar = *pChar
EndIf
*pChar + SizeOf(Character)
Wend
If *outLength
*outLength\i = (*pChar - *String) / SizeOf(Character)
EndIf
If *LastChar=0
ProcedureReturn 0
Else
ProcedureReturn (*LastChar - *String) / SizeOf(Character) + 1
EndIf
CompilerEndIf
EndProcedure
FindCharReverse = @_FindCharReverse() ; Bind ProcedureAddress to the PrototypeHandler
Procedure.s GetFileExtension(File$)
Protected CharPos
CharPos = FindCharReverse(File$, '.')
If CharPos
ProcedureReturn PeekS(@File$+CharPos*2)
EndIf
ProcedureReturn #Null$
EndProcedure
Define File1$ = "c:\test\file.john doe"
Define File2$ = "c:\test\files.list.dat"
Debug File1$
Debug GetFileExtension(File1$)
Debug ""
Debug File2$
Debug GetFileExtension(File2$)