Need Help Making A Viable ExpandServerPath() ColdFusion UDF

Posted July 3, 2007 at 2:39 PM by Ben Nadel

Tags: ColdFusion

ColdFusion provides a built-in function, ExpandPath(), which gives you a fully expanded server path based on the currently executing script. The problem is, a lot of our application templates are NOT on the same level as the currently executing script. Furthermore, if you are using some sort of URL re-writing, the real executing script and the web-visible executing script are not the same values.

What we need is a new function: ExpandServerPath(). This function would act in exactly the same way, only the passed in path (function argument) would be relative to the calling template and would have nothing to do with the executing script.

Back in November, I talked about using a CFTry / CFCatch block with a manually thrown exception to find out the calling template of the given function. I have taken that methodology and wrapped it up in the new ExpandServerPath() ColdFusion user defined function:

  • <cffunction
  • name="ExpandServerPath"
  • access="public"
  • returntype="string"
  • output="false"
  • hint="Expands the given path based on the callind template, not the executing script name.">
  •  
  • <!--- Define arguments. --->
  • <cfargument
  • name="Path"
  • type="string"
  • required="true"
  • hint="The path we are expanded."
  • />
  •  
  • <!--- Define the local scope. --->
  • <cfset var LOCAL = StructNew() />
  •  
  •  
  • <!---
  • Throw an error so that we can get the stack
  • trace from the manually caught exception object.
  • --->
  • <cftry>
  • <cfthrow
  • type="ManualException"
  • message="This is a planned exception."
  • />
  •  
  • <!--- Catch the exception. --->
  • <cfcatch>
  •  
  • <!---
  • Store the tag context array from the
  • exception object. Since the first tag context
  • item will be this template (the one containing
  • the UDF), we will need to grab the second one.
  • --->
  • <cfset LOCAL.Context = CFCATCH.TagContext[ 2 ] />
  •  
  • </cfcatch>
  • </cftry>
  •  
  •  
  • <!---
  • Now that we have our tag context item, we can
  • grab the directory of the template on which this
  • method was called.
  • --->
  • <cfset LOCAL.Template = GetDirectoryFromPath(
  • LOCAL.Context.Template
  • ) />
  •  
  •  
  • <!--- Get the proper slash for this system. --->
  • <cfset LOCAL.Slash = Right(
  • LOCAL.Template,
  • 1
  • ) />
  •  
  •  
  • <!---
  • Clean the path that was passed into the document.
  • We want to switch all slashes to be the proper slash
  • for the expanded path.
  • --->
  • <cfset ARGUMENTS.Path = ARGUMENTS.Path.ReplaceAll(
  • "[\\\/]",
  • "\#LOCAL.Slash#"
  • ) />
  •  
  • <!---
  • Now that we have the proper directory, we are
  • going to need to apply the standard path actions
  • (such as ./ and ../). In order to do so, we are
  • going to loop over the path to expand as if it
  • were a list and apply each element.
  • --->
  • <cfloop
  • index="LOCAL.PathElement"
  • list="#ARGUMENTS.Path#"
  • delimiters="#LOCAL.Slash#">
  •  
  • <!--- Check to see the current path element type. --->
  • <cfif (LOCAL.PathElement EQ ".")>
  •  
  • <!---
  • This is a "same directory" construct. These
  • don't really need to be applied and can just
  • be dropped from the path.
  • --->
  •  
  • <cfelseif (LOCAL.PathElement EQ "..")>
  •  
  • <!---
  • This is a "move up one directory" directive.
  • For this, we need to remove the final slash
  • and directory name from the exsisting template
  • path.
  • --->
  • <cfset LOCAL.Template = LOCAL.Template.ReplaceFirst(
  • "[^\\\/]+[\\\/]$",
  • ""
  • ) />
  •  
  • <cfelse>
  •  
  • <!---
  • We are dealing with an actual path element. Add
  • it to the existing path followed by a slash.
  • --->
  • <cfset LOCAL.Template = (
  • LOCAL.Template &
  • LOCAL.PathElement &
  • LOCAL.Slash
  • ) />
  •  
  • </cfif>
  •  
  • </cfloop>
  •  
  •  
  • <!---
  • At this point, we have an expanded path that has a
  • trailing slash. However, we only want the trailing
  • slash if the path passed into this method had a
  • trailing slash. Other wise, we just want to leave
  • it as it came in (assuming that it was a file).
  • --->
  • <cfif (Right( ARGUMENTS.Path, 1 ) NEQ LOCAL.Slash)>
  •  
  • <!--- Remove last slash. --->
  • <cfset LOCAL.Template = LOCAL.Template.ReplaceFirst(
  • "[\\\/]$",
  • ""
  • ) />
  •  
  • </cfif>
  •  
  •  
  • <!--- Return the expanded path. --->
  • <cfreturn LOCAL.Template />
  • </cffunction>

This function would then just be called like ExpandPath():

  • #ExpandServerPath( "./data/2007_07_02.dat" )#

This would be looking for a DAT file within the directory "data" which is a sub directory of the directory containing the currently executing TEMPLATE (not the root script).

It works, so what's the problem? The problem is, it's not a good methodology. It's one thing to have something like this in a development environment where speed / processing drain is not that critical. But, putting something like this up in a production server is a huge No No. Creating exceptions is a very processing heavy activity. This is certainly not something that we want to be doing all the time.

Does anyone have any suggestions on where to improve this? I tried using the ColdFusion debugging information, but information about a processing template only gets put into it once the template has finished processing (which would not work since we need information about the currently processing template).

Help??



Reader Comments

Jul 3, 2007 at 2:59 PM // reply »
10 Comments

Hey Ben! It was nice meeting you at the CFUNITED conference. Again, thanks for all of the posts.

I'm wondering why you wouldn't use GetBaseTemplatePath(). Doesn't that return the same information that grabbing the TagContext provides? Or am I missing something...


Jul 3, 2007 at 3:15 PM // reply »
11,238 Comments

@Toby,

Really good to meet you to. I don't know why, but when you were talking, you sound exactly like Rob Lowe to me. I don't know if anyone has ever said that or if I have just watched Tommy Boy like a couple hundred too many times :)

As far as GetBaseTemplatePath(), you can actually use something similar to get a relative path, except with GetCurrentTemplatePath(). So, for the example above:

GetDirectoryFromPath( GetCurrentTemplatePath() ) & "data/2007_07_02.dat"

... would give us the same thing. But, to me,

ExpandServerPath( "./data/2007_07_02.dat" )

... just seems more elegant. Of course, I tend to do things that people later will tell me are bad practice, so it's possible that there is no need for this type of a template.

Additionally, my UDF will integrate the path constructs like "./" and "../" right into the returned path (rather than just appending it).

Of course, I might be totally missing something, so if you have an example to look at that you think might help, I would as always, be very greatful.


Jul 3, 2007 at 3:23 PM // reply »
170 Comments

Yeah, use the getBaseTemplatePath() as your starting point. Much faster.


Jul 3, 2007 at 3:26 PM // reply »
11,238 Comments

But, doesn't GetBaseTemplatePath() only return the top-level ColdFusion page. This won't work for any CFIncluded pages that need to get a relative path?


Jul 3, 2007 at 3:27 PM // reply »
170 Comments

@Ben:

It's never good to throw an error when you can avoid it. (Throw/catching errors is slow.) Instead, use the code you posted as your starting point:

GetDirectoryFromPath( GetCurrentTemplatePath() ) & arguments.Path

You can then always clean up the path if you want so that the directory pathing ("." and "..") is cleaned up.


Jul 3, 2007 at 3:29 PM // reply »
170 Comments

@Ben:

If you want the current template, use getCurrentTemplatePath().

That will get the current template you're in.


Jul 3, 2007 at 3:36 PM // reply »
11,238 Comments

@Dan,

Certainly, I do not want to throw and catch errors on the production site. This is a new function, I don't actually use this anywhere. But, I like the idea of it. I wouldn't use it, as I know it is slow. I am posting it up just so people can see what I want to try and do and then offer suggestions. Of course, it might just not be possible.

My current methodology is using the GetCurrentTemplatePath() and all that jazz, but it just never feels elegant.


Jul 3, 2007 at 3:43 PM // reply »
170 Comments

@Ben:

By why don't you use the getCurrentTemplatePath() in this function instead of throwing the error?


Jul 3, 2007 at 3:46 PM // reply »
11,238 Comments

@Dan,

Because the GetCurrentTemplatePath() will give me the path of the template in which the UDF is defined. I need to go up one level to get to the template that invoked the UDF. That's why when I grab out of the Exception tag context, I grab index 2.


Jul 3, 2007 at 5:06 PM // reply »
37 Comments

I can't think of any other way to get the calling template. It would be interesting to see *how* slow your UDF is; is it really that bad performance-wise?


Jul 3, 2007 at 5:10 PM // reply »
11,238 Comments

@Aaron,

Good point. I have never tested this for speed. I have only been told that it is fast. I assume that it is nothing that would scale, but I'd be willing to run a loop :)


Jul 4, 2007 at 3:32 AM // reply »
26 Comments

Ben,

I'm not sure I understand your desired function completely... you want the full path to any file where you give the relative path to the calling template? Well, that's exactly what ExpandPath() does...

ExpandPath( "./data/2007_07_02.dat" ) will give you the full path, so what's the difference to what you need?

Chris


Jul 4, 2007 at 5:51 PM // reply »
11,238 Comments

@Chris,

ExpandPath() goes relative to the base template. For instance, imagine we had this code in the base template:

<!--- Called from top level. --->
#ExpandPath( "./" )#
#ExpandServerPath( "./" )#

<!--- Include sub directory template. --->
<cfinclude template="expand_server_path/test.cfm" />

... And then we had this in the include:

<!--- Called from sub-directory level. --->
#ExpandPath( "./" )#
#ExpandServerPath( "./" )#

... This would give us the following output:

D:\....\site_v1\testing\
D:\....\site_v1\testing\

D:\....\site_v1\testing\
D:\....\site_v1\testing\expand_server_path\

Notice that at the top level template, both in fact do the same thing. However, once you go into an include, ExpandPath() is still relative to the base template, where as ExpandServerPath() is relative to the currently executing template.

Does that clear up the difference in functionality?


Jul 9, 2007 at 12:50 PM // reply »
15 Comments

Hi Ben,

Somewhat related to your post I have a technique here http://www.petefreitag.com/item/630.cfm for finding the application root (the dir containing the acting application.cfm file), you can then use that path and add to it. I know it doesn't solve your problem here but, I thought you might find it handy...


Jul 9, 2007 at 5:57 PM // reply »
11,238 Comments

@Pete,

Thanks. If for no other reason, it's always good for people to see more recursive function examples so they can wrap their heads around it.


Sep 24, 2009 at 5:05 PM // reply »
1 Comments

If I know that the path to my root is 5 slashes into the hard drive, I can use this:

<cfloop index="i" from="5" to="#ListLen(GetBaseTemplatePath(), Chr(92))#">../</cfloop>

to get the proper relationship.


Sep 24, 2009 at 7:06 PM // reply »
11,238 Comments

@Dave,

I am not sure that I follow - wouldn't that take you to the root of the server?



Post A Comment

Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.

Please review the following issues:

Author Name:


Author Email:

Author Website:

Comment:

Supported HTML tags for formatting: <strong>bold</strong>   <em>italic</em>   <code>code</code>







  • Help Wanted - Find Your Next ColdFusion Job
Ben Nadel's Company - Epicenter Consulting Recent Blog Comments
May 20, 2013 at 4:38 PM
Using A Dynamic Column Name With ValueList() In ColdFusion
@Dana, Your confusion is well founded, since this is a very confusing features. In fact, it ONLY works if you use array notation. Meaning, that this: arrayToList( query[ "columnName" ] ) ... read »
May 20, 2013 at 4:34 PM
Using A Dynamic Column Name With ValueList() In ColdFusion
I was thinking chicken and the egg, I wouldn't have expected it to work in the valuelist going in I guess. Maybe I just need a beer, long day :) ... read »
May 20, 2013 at 4:29 PM
Using A Dynamic Column Name With ValueList() In ColdFusion
@Dana, That's if you're trying to reference a specific row. In this case, we're trying to reference the entire query column as one cohesive value. So, you are correct that if you wanted to output a ... read »
May 20, 2013 at 4:24 PM
Using A Dynamic Column Name With ValueList() In ColdFusion
I thought when you used array notation to reference queries you always had to have the row or it would throw a similar error as well? ... read »
May 20, 2013 at 11:45 AM
Using jQuery's Animate() Step Callback Function To Create Custom Animations
This is really useful. I found out that you don't actually have to use a dummy css property (surprisingly). To animate a property in a linear-gradient for instance I did this this.css('someLinearGra ... read »
May 20, 2013 at 10:51 AM
Using A Dynamic Column Name With ValueList() In ColdFusion
@Josh, Oh snap! You're totally right! I'm not sure I've ever tried that. I did know that you can call a number of other array-methods on ColdFusion query columns: http://www.bennadel.com/blog/167 ... read »
May 20, 2013 at 10:45 AM
Using A Dynamic Column Name With ValueList() In ColdFusion
@Ben - I believe you can achieve the same functionality with ColdFusion's built in ArrayToList() function. ArrayToList( users[ "id" ] ); ... read »
May 20, 2013 at 10:21 AM
My Experience With AngularJS - The Super-heroic JavaScript MVW Framework
Is there any error logging and handling framework in angularjs, if not then in what way I can do this. ... read »
InVision App - Prototyping Made Beautiful With Prototyping Tools