• Status: Solved
  • Priority: Medium
  • Security: Public
  • Views: 403
  • Last Modified:

Replicate start command in C

The "start" command seems to be identical to using the "Run" command on the start menu. I'd like to exactly replicate this functionality in a C program. As far as I understand, the start command calls ShellExecute. The problem is: ShellExecute requires two parameters: lpFile and lpParameters. How can I split a command line string to a lpFile and lpParameters exactly like the start command does? The solution must work correctly with quoting.
0
astrand
Asked:
astrand
  • 10
  • 6
  • 5
  • +1
1 Solution
 
Jase-CoderCommented:
int main(int argc, int *argv[])
{
    if(argc != 3)
        printf("Usage: enter program name.");
    else
        ShellExecute(NULL, "open", argv[1], arg[2],  SW_SHOW);

}

I havent compiled this code, but it is how it would work. I am sure I am missing a parameter in ShellExecute(), but I cant test teh code because I dont have windows available.

basically I am checking the command line has three arguments, argc holds this value. The program name is always considered as an argument. The array argv contains every commmand line parameter. Then I simply pass those parameters to the shellexecute function.;
0
 
astrandAuthor Commented:
What about argv[3], argv[4] etc...?

Also, relying on main:s argv[] is not good enough: My application has a WinMain with a LPSTR cmdline, no argv[]. So, I need to translate a command line *string*, just like Start->Run does.
0
 
clockwatcherCommented:
HRESULT Shell(TCHAR* cmdLine, size_t maxLen, TCHAR* lpDirectory)
{
     TCHAR splitChar = ' ';
     size_t splitAt = 0;
     size_t lenCmdLine = 0;
     size_t startAt = 0;
     HRESULT hr = S_OK;

     if (FAILED(hr = StringCchLength(cmdLine, maxLen, &lenCmdLine))) return hr;

     size_t i = 0;
     while ((i < maxLen) && cmdLine[i]==' ') { i++; }  // skip leading spaces
     startAt = i;

     if ( i < maxLen )
     {
          if (cmdLine[i] == '"' || cmdLine[i] == '\'') splitChar = cmdLine[i];
          i++;
          while ( (i < maxLen) &&  cmdLine[i] && (cmdLine[i] != splitChar) ) { i++; }
     }

     splitAt = i;
     TCHAR* lpFile = NULL;
     TCHAR* lpParameters = NULL;

     try  
     {
          lpFile = new TCHAR[maxLen + 1];
          lpParameters = new TCHAR[maxLen + 1];
          *lpParameters = NULL;
          *lpFile = NULL;

          if (FAILED(hr = StringCchCopyN(lpFile, maxLen, (cmdLine + startAt), splitAt + 1 - startAt))) throw hr;
      
          if ( (splitAt + 1) < lenCmdLine)
          {
               if (FAILED(hr = StringCchCopy(lpParameters, maxLen, (cmdLine + splitAt + 1)))) throw hr;
          }

          int shellRetVal = (int) ShellExecute(NULL, _T("open"), lpFile, lpParameters, lpDirectory, SW_SHOW);
          if (shellRetVal < 32) throw MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, 0x200 + shellRetVal);

     } catch (HRESULT result) {
          hr = result;
     }

     delete lpFile;
     delete lpParameters;

     return hr;

}
0
[Webinar] Cloud and Mobile-First Strategy

Maybe you’ve fully adopted the cloud since the beginning. Or maybe you started with on-prem resources but are pursuing a “cloud and mobile first” strategy. Getting to that end state has its challenges. Discover how to build out a 100% cloud and mobile IT strategy in this webinar.

 
astrandAuthor Commented:
This implementation does not work correctly for me:

* When using a cmdline of "notepad c:\foo.txt", lpFile ends up being "notepad " (with a trailing space). This makes ShellExecute fail.

* The function does not correctly handle spaces. If I call it with:

Shell("c:\\temp\\program files\\foo.rtf", 1024, "c:\\");

...lpFile ends up being c:\temp\program, which is not correct, and causes ShellExecute to fail.
0
 
clockwatcherCommented:
The first problem is easy to fix.  I broke it to catch the trailing ".  

// shellexecute.cpp : Defines the entry point for the application.
//

#include "stdafx.h"
#include <windows.h>
#include <shellapi.h>
#include <strsafe.h>

HRESULT Shell(TCHAR* cmdLine, size_t maxLen, TCHAR* lpDirectory);

int APIENTRY _tWinMain(HINSTANCE hInstance,
                     HINSTANCE hPrevInstance,
                     LPTSTR    lpCmdLine,
                     int       nCmdShow)
{
      const size_t MAXLEN = 255;

      TCHAR cmd1[MAXLEN + 1] = _T("notepad c:\\test.txt");
      TCHAR cmd2[MAXLEN + 1] = _T("\"c:\\program files\\mozilla firefox\\firefox.exe\" http://www.google.com");

      Shell(cmd1, MAXLEN, NULL);
      Shell(cmd2, MAXLEN, NULL);

}

HRESULT Shell(TCHAR* cmdLine, size_t maxLen, TCHAR* lpDirectory)
{
      TCHAR splitChar = ' ';
      size_t splitAt = 0;
      size_t lenCmdLine = 0;
      size_t startAt = 0;
      HRESULT hr = S_OK;
      int keepSplit = 0;

      if (FAILED(hr = StringCchLength(cmdLine, maxLen, &lenCmdLine))) return hr;

      size_t i = 0;
      while ((i < maxLen) && cmdLine[i]==' ') { i++; }  // skip leading spaces
      startAt = i;

      if ( i < maxLen )
      {
            if (cmdLine[i] == '"' || cmdLine[i] == '\'')
            {
                  keepSplit = 1;
                  splitChar = cmdLine[i];
            }

            i++;
            while ( (i < maxLen) &&  cmdLine[i] && (cmdLine[i] != splitChar) ) { i++; }
      }

      splitAt = i;
      TCHAR* lpFile = NULL;
      TCHAR* lpParameters = NULL;

      try
      {
            lpFile = new TCHAR[maxLen + 1];
            lpParameters = new TCHAR[maxLen + 1];
            *lpParameters = NULL;
            *lpFile = NULL;

            if (FAILED(hr = StringCchCopyN(lpFile, maxLen, (cmdLine + startAt), splitAt + keepSplit - startAt))) throw hr;
      
            if ( (splitAt + 1) < lenCmdLine)
            {
                  if (FAILED(hr = StringCchCopy(lpParameters, maxLen, (cmdLine + splitAt + keepSplit)))) throw hr;
            }

            int shellRetVal = (int) ShellExecute(NULL, _T("open"), lpFile, lpParameters, lpDirectory, SW_SHOW);
            if (shellRetVal < 32) throw MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, 0x200 + shellRetVal);

      } catch (HRESULT result) {
            hr = result;
      }

      delete lpFile;
      delete lpParameters;

      return hr;

}

But there's not a whole lot of point in fixing it, if you really want the second to work too-- too be able to call it without quotes.  Rolliing it by hand, you'd have to do a directory traversal to see where the directory ends and the command starts.  Thinking about it, it really probably wouldn't be that difficult to do, but I'd rather not code it up in oldschool C/C++.

Maybe someone else will come along with an existing API that already does it for you.  Sorry I wish I knew of one for you.
0
 
clockwatcherCommented:
Not the most elegant of solutions but it handles the ones you've thrown out so far:

// shellexecute.cpp : Defines the entry point for the application.
//

#include "stdafx.h"
#include <windows.h>
#include <shellapi.h>
#include <strsafe.h>

HRESULT Shell(TCHAR* cmdLine, size_t maxLen, TCHAR* lpDirectory);
size_t BruteForceIt(TCHAR* cmdLine, size_t maxLen);

int APIENTRY _tWinMain(HINSTANCE hInstance,
                     HINSTANCE hPrevInstance,
                     LPTSTR    lpCmdLine,
                     int       nCmdShow)
{
      const size_t MAXLEN = 255;

      TCHAR cmd1[MAXLEN + 1] = _T("notepad c:\\test.txt");
      TCHAR cmd2[MAXLEN + 1] = _T("c:\\program files\\mozilla firefox\\firefox.exe http://www.google.com");
      TCHAR cmd3[MAXLEN + 1] = _T("\"c:\\program files\\mozilla firefox\\firefox.exe\" http://www.yahoo.com");
      TCHAR cmd4[MAXLEN + 1] = _T("\\program files\\mozilla firefox\\firefox.exe http://www.msn.com");

      Shell(cmd1, MAXLEN, NULL);
      Shell(cmd2, MAXLEN, NULL);
      Shell(cmd3, MAXLEN, NULL);
      Shell(cmd4, MAXLEN, NULL);

}

HRESULT Shell(TCHAR* cmdLine, size_t maxLen, TCHAR* lpDirectory)
{
      TCHAR splitChar = ' ';
      size_t splitAt = 0;
      size_t lenCmdLine = 0;
      size_t startAt = 0;
      HRESULT hr = S_OK;
      int keepSplit = 0;

      if (FAILED(hr = StringCchLength(cmdLine, maxLen, &lenCmdLine))) return hr;

      size_t i = 0;
      while ((i < maxLen) && cmdLine[i]==' ') { i++; }  // skip leading spaces
      startAt = i;

      splitAt = BruteForceIt(cmdLine, maxLen);

      if ( 0 == splitAt  )
      {

            if (i < maxLen )
            {
                  if (cmdLine[i] == '"' || cmdLine[i] == '\'')
                  {
                        keepSplit = 1;
                        splitChar = cmdLine[i];
                  }

                  i++;
                  while ( (i < maxLen) &&  cmdLine[i] && (cmdLine[i] != splitChar) ) { i++; }
      
            }
            splitAt = i;

      }

      TCHAR* lpFile = NULL;
      TCHAR* lpParameters = NULL;

      try
      {
            lpFile = new TCHAR[maxLen + 1];
            lpParameters = new TCHAR[maxLen + 1];
            *lpParameters = NULL;
            *lpFile = NULL;

            if (FAILED(hr = StringCchCopyN(lpFile, maxLen, (cmdLine + startAt), splitAt + keepSplit - startAt))) throw hr;
      
            if ( (splitAt + 1) < lenCmdLine)
            {
                  if (FAILED(hr = StringCchCopy(lpParameters, maxLen, (cmdLine + splitAt + keepSplit)))) throw hr;
            }

            int shellRetVal = (int) ShellExecute(NULL, _T("open"), lpFile, lpParameters, lpDirectory, SW_SHOW);
            if (shellRetVal < 32) throw MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, 0x200 + shellRetVal);

      } catch (HRESULT result) {
            hr = result;
      }

      delete lpFile;
      delete lpParameters;

      return hr;

}

size_t BruteForceIt(TCHAR* cmdLine, size_t maxLen)  
{
      size_t len = 0;
      size_t splitAt = 0;

      if SUCCEEDED(StringCchLength(cmdLine, maxLen, &len))
      {
            TCHAR* buffer = new TCHAR[len + 1];
            
            if (buffer && SUCCEEDED(StringCchCopy(buffer, len + 1, cmdLine)))
            {
                  for (size_t i=1; i < len; i++)
                  {
                        buffer[i] = NULL;
                        if (GetFileAttributes(buffer) != INVALID_FILE_ATTRIBUTES) splitAt = i;
                        buffer[i] = cmdLine[i];
                  }
            }
            delete buffer;
      }

      return splitAt;
}
0
 
astrandAuthor Commented:
Close, but no cigar. I have a directory called "c:\temp\program files" and also a program called "c:\temp\program.exe". If I call your Shell function with "c:\temp\program files\foo.rtf", the "c:\temp\program files" folder is opened, but with Windows built-in Start->Run, program.exe is executed, with files\foo.rtf as argument.
0
 
clockwatcherCommented:
It's easy to tack on the check for ".exe" and ".bat" which I'm pretty sure are the only extensions that it would execute without explicitly including the extension (".com" might be a holdover from dos 3 days -- who knows).  But, it's easy to find other exceptions-- you can also type "control panel" in Start->Run and have it pull up the control panel.  Maybe someone else knows what it's actually calling to parse it.  Anyway goodluck.

BTW, what does Start->Run do if you actually have both of these files:

c:\temp\program.exe
c:\temp\program files\foo.rtf

And, you use:
 
   c:\temp\program files\foo.rtf

Does it launch program.exe or does it open the rtf?
0
 
astrandAuthor Commented:
>It's easy to tack on the check for ".exe" and ".bat" which I'm pretty sure are the only extensions that it would execute without explicitly >including the extension (".com" might be a holdover from dos 3 days -- who knows).

My guess is that either it only checks for .exe (like CreateProcess does) or that all extensions from PATHEXT are checked.

>But, it's easy to find other exceptions-- you can also type "control panel" in Start->Run and have it pull up the control panel.  

This is no magic - there's a file called control.exe in system32.

>BTW, what does Start->Run do if you actually have both of these files:
>
>c:\temp\program.exe
>c:\temp\program files\foo.rtf

Windows opens up the RTF file in this case.

I'm surprised that something basic as this is should be this difficult.
0
 
clockwatcherCommented:
Nope on PATHEXT.  Not on something that's qualified (at least not in the tests that I did).  And if it's not qualified it should be a non-issue and get picked up by the first split anyway.  But, Start->Run definitely runs both ".bat" and ".exe" on a qualified path even if you don't give it the extension.  Here's a fixed up version handling the .bat and .exe.  

Give it a shot and see what you can come up with to break it.


// shellexecute.cpp : Defines the entry point for the application.
//

#include "stdafx.h"
#include <windows.h>
#include <shellapi.h>
#include <strsafe.h>

HRESULT Shell(TCHAR* cmdLine, size_t maxLen, TCHAR* lpDirectory);
size_t FindLongestValidFileName(TCHAR* cmdLine, size_t maxLen, size_t startAt);

int APIENTRY _tWinMain(HINSTANCE hInstance,
                     HINSTANCE hPrevInstance,
                     LPTSTR    lpCmdLine,
                     int       nCmdShow)
{
      const size_t MAXLEN = 255;

      const int NRUNTHIS = 7;
      TCHAR* runThis[] = {
            _T("\"c:\\temp\\program files\\lotnumber.txt\""), /* both of these should open lot number */
            _T("c:\\temp\\program files\\lotnumber.txt"),  
            _T("c:\\temp\\program files\\abc.txt"), /* should launch program.exe */
            _T("control panel"),
            _T("notepad c:\\temp\\program files\\lotnumber.txt"), /* sanity checks */
            _T("\"c:\\program files\\mozilla firefox\\firefox.exe\" http://www.yahoo.com"),
            _T("\\program files\\mozilla firefox\\firefox.exe http://www.msn.com")
      };

      for (int i=0; i < NRUNTHIS; i++)
      {
        Shell(runThis[i], MAXLEN, NULL);
      }

}

HRESULT Shell(TCHAR* cmdLine, size_t maxLen, TCHAR* lpDirectory)
{
      TCHAR splitChar = ' ';
      size_t splitAt = 0;
      size_t lenCmdLine = 0;
      size_t startAt = 0;
      HRESULT hr = S_OK;
      int keepSplit = 0;

      if (FAILED(hr = StringCchLength(cmdLine, maxLen, &lenCmdLine))) return hr;

      size_t i = 0;
      while ((i < maxLen) && cmdLine[i]==' ') { i++; }  // skip leading spaces
      startAt = i;

      splitAt = FindLongestValidFileName(cmdLine, maxLen, startAt);

      // couldn't find a valid file name or it starts with a quote
      // split it at the next quote or the first space
      if ( 0 == splitAt  )
      {
            if (i < maxLen )
            {
                  if (cmdLine[i] == '"')
                  {
                        keepSplit = 1;
                        splitChar = cmdLine[i];
                  }
                  i++;
                  while ( (i < maxLen) &&  cmdLine[i] && (cmdLine[i] != splitChar) ) { i++; }
            }
            splitAt = i;
      }

      TCHAR* lpFile = NULL;
      TCHAR* lpParameters = NULL;

      try
      {
            lpFile = new TCHAR[maxLen + 1];
            lpParameters = new TCHAR[maxLen + 1];
            *lpParameters = NULL;
            *lpFile = NULL;

            if (FAILED(hr = StringCchCopyN(lpFile, maxLen, (cmdLine + startAt), splitAt + keepSplit - startAt))) throw hr;
      
            if ( (splitAt + 1) < lenCmdLine)
            {
                  if (FAILED(hr = StringCchCopy(lpParameters, maxLen, (cmdLine + splitAt + keepSplit)))) throw hr;
            }

            int shellRetVal = (int) ShellExecute(NULL, _T("open"), lpFile, lpParameters, lpDirectory, SW_SHOW);
            if (shellRetVal < 32) throw MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, 0x200 + shellRetVal);

      } catch (HRESULT result) {
            hr = result;
      }

      delete lpFile;
      delete lpParameters;

      return hr;

}


size_t FindLongestValidFileName(TCHAR* cmdLine, size_t maxLen, size_t startAt)  
{
      // returns the split point within cmdLine where the longest valid file name exists

      size_t len = 0;
      size_t splitAt = 0;

      if (cmdLine[startAt] == '"') return 0;  // quotes take precedence over this

      if SUCCEEDED(StringCchLength(cmdLine, maxLen, &len))
      {
            const int EXTLENGTH = 4; //extension (.bat, .exe) length

            TCHAR* buffer = new TCHAR[len + EXTLENGTH + 1];  

            const int executableExtensionCount = 2;
            const TCHAR* executableExtensions[] = { _T(".bat"), _T(".exe") };

            if (buffer && SUCCEEDED(StringCchCopy(buffer, len + 1, cmdLine)))
            {
                  for (size_t i= startAt + 1; (i <= len); i++)
                  {
                        if (cmdLine[i] == ' ' || (i == len)) // at a stop point. is it valid?
                        {
                              // does it exist in the file system?
                              buffer[i] = NULL;
                              if (GetFileAttributes(buffer) != INVALID_FILE_ATTRIBUTES) splitAt = i;
                              buffer[i] = cmdLine[i];
                              
                              // does an executable with that name exist there
                              for (int iExec=0; iExec < executableExtensionCount; iExec++)
                              {
                                    _tcscpy(buffer + i, executableExtensions[iExec]);
                                    buffer[i + EXTLENGTH] = NULL;
                                    if (GetFileAttributes(buffer) != INVALID_FILE_ATTRIBUTES)
                                    {
                                          splitAt = i;
                                    }
                              }
                              StringCchCopy(buffer, len + 1, cmdLine); // fix the string back up                              
                        }
                  }
            }
            delete buffer;
      }

      return splitAt;
}
0
 
clockwatcherCommented:
Nevermind  It won't handle URIs right:   http://www.google.com/this is a test.  Under start->run, that ends up with:  http://www.google.com/this%20is%20a%20test

 -- http://msdn.microsoft.com/library/default.asp?url=/workshop/networking/pluggable/overview/appendix_a.asp

It would need to add a check if it was in the format of a URI type reference (uri://) and if it was pass off the entire string as lpFile.  

Easy to add, but then it brings up what the heck else is missing.
0
 
clockwatcherCommented:
Without full specs on all that Start->Run does, it's going to be hard to get the exact same functionality from a ShellExecute call.  I can fix the URL issue if you want, but I'm sure something else will come up.
0
 
astrandAuthor Commented:
Since no-one could solve this problem, I'd like a refund.
0
 
DanRollinsCommented:
Soory to be late to the party, but I just now saw this.

I'm pretty sure that what you are looking for is a method to have the system parse it exactly as it would on the command prompt command line.  This will do that:

      CString sCmdLine= "C:\\temp\\junk.txt";

      CString sCmd = "Cmd.Exe";
      CString sParms = "/cSTART ";
              sParms+= sCmdLine;
      ShellExecute( 0, "open", sCmd, sParms, NULL,  SW_HIDE );

It starts a command processor and executes the START command (with /c, which means close when done).  It also indicates to hide the ugly black command window that would otherwise appear.  Cmd.Exe uses whatever techniques it *usually* uses to determine what program to launch and what parameters to send that program.

-- Dan
0
 
clockwatcherCommented:
Dan,

This:

#include "stdafx.h"
#include <windows.h>
#include <shellapi.h>

int _tmain(int argc, _TCHAR* argv[])
{
   TCHAR sCmd[] = _T("Cmd.Exe");
   TCHAR sParms[] = _T("/cSTART http://www.google.com/this is a test");
   ShellExecute( 0, "open", sCmd, sParms, NULL,  SW_HIDE );
}

Doesn't get the same as Start, Run:  

    http://www.google.com/this is a test

It's not doing the same thing.  Maybe it does enough of what s/he wants (mine didn't for whatever reason), but it's definitely not the same thing.
0
 
DanRollinsCommented:
The Cmd.Exe /C START trick does work exactly like the command line, but it appears that the Start/Run does handle a few screwball variations somewhat differently.

Here's an alternative I cam across here:
  http://msdn.microsoft.com/msdnmag/issues/05/03/CATWork/

The URL.DLL library has a tool that analyses a string of text and figures out how to treat it as a command and "start" that command.  It lokks like it accesses any IShellExecuteHook handlers and otherwise reacts the same as the text in the
   Start/Run...
box

//      LPCTSTR szUrl = _T("www.microsoft.com");
//      LPCTSTR szUrl = _T("http://www.google.com/this is a test");
//      LPCTSTR szUrl = _T("C:\\temp\\junk.txt");
      LPCTSTR szUrl = _T("C:\\temp\\program files\\junk.txt");

      CString sArgs;
      sArgs.Format( "url.dll,FileProtocolHandler %s", szUrl );
      ShellExecute( NULL, "open", "rundll32.exe",  sArgs,  NULL, SW_SHOWNORMAL );

0
 
astrandAuthor Commented:
The problem with this last comment is that this solution doesn't accept strings like "notepad c:\foo.txt".
0
 
DanRollinsCommented:
I do believe that this variation covers all of the bases.  
It handles URLs separately.  
If there exists an executable (c:\temp\program) then it takes the same action as the Start>Run... command -- which varies depending on if the whole string represents an existing files (if not, it runs c:\temp\program.exe)

#include <shlwapi.h>
#pragma comment( lib,"shlwapi.lib" )

void CD31Dlg::OnButton1()
{

//  CString sCmd= "www.microsoft.com";
//  CString sCmd= "http://www.google.com/this is a test";
//  CString sCmd= "C:\\temp";                        // a directory name
//  CString sCmd= "C:\\temp\\junk.txt";
//  CString sCmd= "C:\\temp\\program files\\junk.txt";  // a doc that exists
//  CString sCmd= "C:\\temp\\program files\\junk1.txt"; // a doc that does not exist
  CString sCmd= "notepad C:\\temp\\junk.txt";  // a regular command

      if ( PathIsURL( sCmd ) ) {
            CString sArgs;
            sArgs.Format(_T("url.dll,FileProtocolHandler %s"), sCmd );
            ShellExecute( NULL, _T("open"), _T("rundll32.exe"), sArgs, NULL, SW_SHOWNORMAL  );
      }
      else {  // not a URL
            char buf[1000];
            int nRet= GetShortPathName( sCmd, buf, sizeof(buf) );
            if (nRet == 0 ) { // file does not exist (could be "notepad file.txt")
                  sCmd.Insert(0,"/c START " );
                  ShellExecute( 0, "open", "Cmd.Exe", sCmd, NULL,  SW_HIDE );
            }
            else {   // specified cmd is not an existing file
                  sCmd= buf;
                  sCmd.Insert(0,"/c START " );
                  ShellExecute( 0, "open", "Cmd.Exe", sCmd, NULL,  SW_HIDE );
            }
      }
}
0
 
clockwatcherCommented:
I know both of us (Dan and I) put time into this question and I think it's worth a PAQ.  But Dan's last post still doesn't capture the Start, Run functionality correctly.

  CString sCmd= "\"c:\\test.txt\"";

Doesn't shell notepad if the file c:\test.txt exists.  It does with Start, Run.

It doesn't seem astrand should have to pay the points.  It also doesn't seem Dan should lose the points either.  Is there a way to give the points to Dan without docking them from astrand?
0
 
DanRollinsCommented:
If the only dfference is that is that the command text is quoted, then it would be a very easy operation to strip those quotes before making the call to GetShortPathName.

Frankly, I feel that I have gotten (at least) VERY close to a solution that will perform as expected and I deserve the full points for this question.
0
 
clockwatcherCommented:
"IF the only difference is"

But, I agree.  You should get the points for the question.  But it would be nice to have the actual call that the shell is making.
0
 
DanRollinsCommented:
There is no guarantee that the Windows internal program code that handles the Start/Run command it is making any single function call.  It may well be doing a combination of things... handling special cases, looking for URLs, etc.  If there is one, it would appear to be an undocumented intenal function and not something published in the API documentation.
0

Featured Post

New feature and membership benefit!

New feature! Upgrade and increase expert visibility of your issues with Priority Questions.

  • 10
  • 6
  • 5
  • +1
Tackle projects and never again get stuck behind a technical roadblock.
Join Now