2007-05-31

PHP spaghetti

Last couple of days I've been toying with the idea of XML template-based metaprogramming. Essentially, the idea is to create a simple HTML page with some namespaced XML template elements (similar to Blogger new layout tags) that would compile down to plain old PHP spaghetti before deploying. In other words, to have something like this:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" 
                      "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" 
      xmlns:tpl='http://malatestapunk-stuff.blogspot.com'  
      xml:lang="en" 
      lang="en">
    <tpl:set data="a_variable" value="a value" />
    <tpl:set data="another_variable" action="FunctionName('arg1', 'arg2')" />

    <tpl:if cond="data:a_variable == data:another_variable">
        <tpl:data source="YetAnotherVarFromIncludedFile" />
    </tpl:if>

    <head>
        <title><tpl:fetch source="TableArticles" data="Title" cond="id=1" /></title>
    </head>
    <body>
        <tpl:loop source="TableArticles" cond="author='anyone'" as="Article">
            <li>
                <strong><tpl:data source="Article/Title" /></strong>
                <em><tpl:data source="Article/Author" /></em>

                <p> <tpl:data source="Article/Body" action="ClassName::methodName('arg2', 'arg3')" /> </p>

                <p> <tpl:data action="Solo" /> </p>
            </li>
        </tpl:loop>
    </body>
</html>

turned into something like this:

<?php
include ('config.php');
include ('actions.php');
$link = mysql_connect(DATABASE_HOST, DATABASE_USERNAME, DATABASE_PASSWORD);
mysql_select_db(DATABASE_NAME);
?>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" 
                      "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:tpl="http://malatestapunk-stuff.blogspot.com" xml:lang="en" lang="en">
    <?php $a_variable = 'a value';  $another_variable = FunctionName('arg1', 'arg2');  if ($a_variable == $another_variable) {  echo $YetAnotherVarFromIncludedFile;  } ?>

    <head>
        <title><?php 
$_db_result = mysql_query("SELECT Title FROM " . TABLE_PREFIX . "TableArticles WHERE id=1");
$Title = mysql_fetch_array($_db_result, MYSQL_NUM);
echo $Title [0];
?></title>

    </head>

    <body>
        <?php 
$_db_result = mysql_query("SELECT * FROM " . TABLE_PREFIX . "TableArticles WHERE author='anyone'");
while ($Article = mysql_fetch_array($_db_result, MYSQL_ASSOC)) {
?>
            <li>
                <strong><?php echo $Article['Title']; ?></strong>, by
                <em><?php echo $Article['Author']; ?></em> said:
                <p><?php echo ClassName::methodName($Article['Body'], 'arg2', 'arg3'); ?></p>
                <p><?php echo Solo(); ?></p>
            </li>
        <?php } ?>
    </body>
</html>

before deploying it on the server.

Reasoning

While this approach may not be good enough for any serious stuff, if all you need is to get some data from the database and massage it into some XHTML (which is very often the case), it can be a fast and easy way out. By altering only templates, you keep your code/logic separate from your presentation, which is, of course, very important. However, when compiled to a script (which could easily be done either locally - say with make or a similar tool - or server-side) and deployed to a server, it most likely will outperform a more robust solution because of it's specific scope and thus, a smaller footprint.

Another advantage would be simplified RDBMS switching by extending the main parser class and rebuilding the page scripts, without the added overhead of introducing a database abstraction layer to the script itself. And, compared to working with a full-blown framework, this approach allows for a much shorter development cycle, which is kinda cool for rapid prototyping.

2007-05-19

Advanced tips for Total Commander

As I hinted in my previous post on Total Commander, there are lots of ways it can help you in your day to day tasks. There are all sorts of addons available, but you can make it do even more - a little bit of shell scripting and documentation reading goes a long way here, because 5 minutes spent today can (and will) save you hours in the future.
There are a couple of (easy) ways you can extend TC functionality with your own actions, tailored to suit your workflow. You can either use TC internal commands, or make your scripts accessible from the button bar, the Start menu, or the directory hotlist. There is a big advantage to the button bar and start menu usage, as you get the special runtime parameters you can pass to your scripts. However, I find these harder to access then the directory hotlist. I have Ctrl+D hardcoded in my fingers and it's right in front of my eyes all the time, so I'll stick to it.

The Directory Hotlist menu

Directory hotlist button in TC Sure, we all add our frequently used directories there, and some of us perhaps even organize it in sections. But we can make it do more useful things - there are a lot of builtin TC commands available from there and plus, anything that can be executed from the shell can be executed from there as well. Just open the menu (either with your mouse, or by pressing Ctrl+D), and select Configure. In a new window you'll get, click Add Item button, and enter the name for your new action. Then, either select an internal command from the Command: dropdown, or enter the shell command you wish to be executed. For an example:
cmd /c "tree > tree.txt"
This will create a file named tree.txt in your currently open directory, with the tree of its subdirectories. OK, so this wasn't very useful but you get the picture. Moving along.
TC Command dialog
Now, suppose I have some movies on my HD that I want to know more about. It is quite trivial to find the information on the 'net - I'd just go to IMDb and do a search for the title. However, it is a multi-step operation: open my browser, type the address, type the movie title, hit search. Obviously, that should be easier - I mean, that's why we have computers in the first place, right? Right. So let's make a script to automate this task for us and make it easily accessible in TC hotlist.

What you sed is what you get

Note: you will need some special tools for this - namely, sed and gawk (you should have them around anyway - it's really good stuff). Fortunately, they're freely available as a part of UnxUtils package, a porting effort of some common GNU utilities to native Win32.
After you download the archive, unpack the sed.exe and gawk.exe from usr/local/wbin to directory in your path (eg. C:\WINDOWS).
Thinking about it, almost every movie I encountered was in a directory named after its title. So let's use that as the starting point:
cd | sed "s/.*\\//"
We use cd output to find out the current working directory. Unfortunately, cd outputs the full path (eg. D:\Desktop\Batch) instead of what we need. That's why we sent the output to sed - this will replace everything up to the last \ with nothing (if you'd like to know more about sed, there are a lot of good tutorials around. You may want to try this one).
There is yet another problem: cmd.exe is not cool at all. We can't just assign the output we just got to a shell variable in order to use it later - not in a straightforward way (using SET), anyway. However, there is a way to do just that:
cd | sed "s/.*\\/set directory_name=/" > tmp_.bat
call tmp_.bat
del tmp_.bat
The code above will replace everything up to the last \ from the cd output with set directory_name= and dump that in a file named tmp_.bat, so the newly created file would contain something like:
set directory_name=Whatever Directory Name
Next, when we CALL the tmp_.bat, it will import the directory_name environment variable into the current namespace. After that, we don't need the intermediate script anymore, so we delete it (using DEL). And the hard part is actually over: the only thing left is to do the search. Fortunately, IMDb accepts GET search requests, so this is trivial:
"C:Program Files\Mozilla Firefox\firefox.exe" "http://www.imdb.com/find?s=tt&q=%directory_name%"
This will open [Firefox] at the given URL - which is the IMDb search results we were after all along. Now, we wrap this up in a batch file:
cd | sed "s/.*/set nme=/" > tmp_.bat
call tmp_.bat
del tmp_.bat
"C:Program Files\Mozilla Firefox\firefox.exe" "http://www.imdb.com/find?s=all&q=%nme%"
and save it somewhere. Next, we do Ctrl+D -> Configure -> Add Item, enter a descriptive name for our new action, and enter this at the Command: line:
cmd /c "Full_Path_To_Your_Batch_File.bat"
Every time you execute your new action, the script will run in the current directory - the one you're browsing in currently active TC pane.
Note: there may be more elegant ways of doing such things, but I like to keep my scripts hackish and simple. For me, a shell script is a quick and dirty way out - it should be intuitive to write and quickly provide the results without too much hassle. Anything that needs to be elegant should be done in a proper programming language anyway.

Search for more info on MP3s

I like to know stuff about what I'm listening to. There is a lot of information available on the web about different artists and albums, and I find myself often doing repetitive work to get to it. However, it is quite easy to get all (or most) of it at once with a simple script, similar to the one we just did.
However big your MP3 collection is, chances are that most of the files are in some of the most frequent folder schemes - either ArtistName\AlbumName or ArtistName - AlbumName. The approach is very similar for both cases, we'll use gawk to extract the information from the path provided by cd command. First, the ArtistName\AlbumName case:
cd | gawk -F\\  "{print (\"set artist=\" $(NF-1) \"\nset album=\" $NF)}" > tmp_.bat
We'll use the same method to obtain the command output as environment variables, but using a different tool (gawk) to get it done. First we declare \ to be a field separator - that means that gawk will split into separate fields whatever we give it at each \. We can access fields by attaching $ to the number of field that we want, left-to-right ($1 for the first one, $2 for the second, etc). Since we want the one on the far right, we use NF - it means number of fields in the gawk lingo. We also need the one just before the last, so we subtract 1 from the number of fields - that's what $(NF-1) means.
If none of this makes sense to you, don't worry. Awk (gawk is just a flavor of awk) is very well documented, and there are a lot of tutorials around. The basics are covered here, and if you'd like to learn more, try this one.
We also need to print it, and therefore we need double quotes. That is why we escape the quotes in the print statement by adding \ in front of them. This way, cmd.exe will leave those quotes alone, and let gawk parse it instead. Note that we need each SET statement to be on the separate line in the intermediate batch file. That's why we separate them with \n, which means "insert newline here".
The ArtistName - AlbumName case is quite similar. We'll build on what we got from the previous scripts:
cd | sed "s/.*\\//" | gawk -F- "{print (\"set artist=\" $1 \"\nset album=\" $2)}" > tmp_.bat
So, first we extract the last directory in the current path, just like in the IMDb search script. Next, we declare '-' to be field separator because we want to split the directory name at dash character. Since we assume that artist name is on the left side of the dash, and everything else to be album name (we'll be doing a web search, so we don't have to get surgical about this), we can use simple $1 and $2.
Once we have our %artist% and %album% variables set in the intermediate batch file, the rest of the script is the same for both cases. We use the old technique to get to them and then we put them to use:
REM ...
call tmp_.bat
del tmp_.bat

REM +---------------------------------------------------------------------------------
  REM   This is where we set up search URLs. 
  REM   Comment out the ones you don't need, or add some more.
REM +---------------------------------------------------------------------------------
set discogs="http://www.discogs.com/search?type=artist&q=%artist%" 
set cduni_artist="http://www.cduniverse.com/sresult.asp?HT_Search_Info=%artist%&HT_Search=ARTIST" 
set cduni_album="http://www.cduniverse.com/sresult.asp?HT_Search_Info=%album%&HT_Search=TITLE" 
set gimg_artist="http://images.google.com/images?q=%artist%" 
set gimg_album="http://images.google.com/images?q=%album%" 
set gimg_both="http://images.google.com/images?q=%artist% %album%" 
set wiki="http://en.wikipedia.org/wiki/Special:Search?search=%artist%" 

REM +---------------------------------------------------------------------------------
  REM   OK, were all set, let's do some searching:
REM +---------------------------------------------------------------------------------
"C:Program Files\Mozilla Firefox\firefox.exe" %discogs% %cduni_artist% %cduni_album% %gimg_artist% %gimg_album% %gimg_both% %wiki%
Now, we just substitute the REM ... line with the appropriate gawk command, and the script is ready to go into the directory hotlist as a brand new action.