Page 1 of 1

Create a custom Unix/Linux command A "list only directories" command that works like "ls" Rate Topic: -----

#1 jumptrooper  Icon User is offline

  • D.I.C Head
  • member icon

Reputation: 68
  • View blog
  • Posts: 234
  • Joined: 19-August 10

Posted 03 September 2010 - 10:15 AM

In this tutorial we are going to create a little system tool that will hopefully be very useful.

If you work on a Unix/Linux system you've no doubt used the 'ls' command enough times to have it firmly established in your muscle memory. This command is indispensable! Unfortunately, sometimes you just want to know about the directories and not all the files around them. Wouldn't it be cool to have a command like 'ls' that you could type from anywhere in the command line to get that list of directories!? Indeed it would! That's what we're going to make today - our very own Unix/Linux command that has all the functionality of 'ls', but only bothers to show you directories. Sadly, the name 'ld' was already taken by the 'GNU Linker' so I'm going to settle for calling the command 'ldir' (list directories), but you can call yours whatever you like.

To start, we'll create the basic program. Once that's working, we'll add an additional feature to make it work with all of the flags available to 'ls'. Finally, we'll integrate it with the operating system so that it can be used system-wide.

First, create a file named "ldir" (no need to put a file extension). Then, open it up in your text editor and type the "shebang line" at the start of the file:
#!/usr/bin/perl -w


This tells the operating system where to find Perl when it tries to interpret the script. If your Perl executable is located somewhere else, make the appropriate changes.

Next, I'm going to suggest using the "strict" module to enforce strict error checking. This helps ensure everything is working as intended by pointing out all possible errors - Perl will let a lot of things slide if you don't.
use strict;


Now, lets collect all our arguments passed in from the command line:
my $args = join(' ', @ARGV);


As an example, typing "perl ldir /" to execute your script, passes "/" into the program as an argument. "/" is stored in the array "@ARGV", which is a default system variable of Perl's. Using the "join(' ', @ARGV)" command converts the array into a string with each array element separated by a ' '. Now we have the location we're going to operate on and possibly some flags (or nothing).

Next, let's use the system's 'ls' command to do the bulk of our dirty work and store its results in a variable. Notice that those are "back-ticks" and not single quotes (the back-tick symbol is located on the same key as the tilde "~"). Back-ticks instruct Perl to execute a system command and capture the output.
my $mystring = `ls -l $args`;


Now we have all the contents of a directory (files and all) stored in $mystring. If you noticed, we used the "-l" flag to make 'ls' print all the gory details of the directory. We did this so that we can exploit the fact that every line printed that by that command that describes a directory is prefixed with a "d" - like this:
...
-rw-r--r-- 1 hubbell hubbell 18 2010-08-14 19:31 PASS
drwxr-xr-x 3 hubbell hubbell 4096 2010-08-26 20:05 php
-rwxr-xr-x 1 hubbell hubbell 1738 2010-08-14 19:54 scanner
-rw-r--r-- 1 hubbell hubbell 3849 2010-08-29 19:23 script.js
-rw-r--r-- 1 hubbell hubbell 11861 2010-08-29 18:59 style.css
drwxr-xr-x 2 hubbell hubbell 4096 2010-08-14 19:08 test
...
We can easily pick out the lines that correspond to directories with regular expressions. But first we have to actually separate things out into different lines. As it is, it may look like multiple lines of text, but Unix sees it as a single string with invisible little newline characters ("\n") sprinkled about making it only appear to have more than one line. We need to split the string up so that our results are actually on new lines so our pattern matching will work the way we want it to. For this, we use the "split()" function to split up the string and store it in an array with each element being a single line of output.
my @dirs = split('\n', $mystring);


With each line an element of the array "@dirs", we can use a 'foreach' loop to evaluate each line.
foreach(@dirs){
  ...
}


We want to select lines that begins with a 'd' and pull out the last word of that line (which is the name of the directory):
if(m/^d/){
  $_ =~ m/\s(\S+$)/;
  print "$1\n";
}

The funky looking line in the middle reads "Take the current line from "@dirs" ($_) and apply this regular expression to it (=~) : match (m) anything that has a blank spot (\s) and one or more non-blank characters (\S+) that are at the end of the string ($) and save those non-blank charcters in the variable $1 (which is what the parentheses around the "\S+$" does). Then we just print the resulting string, which should be just the name of the directory.

That's it! However we can make this better. Currently, the program will accept all the normal flags 'ls' will accept because they get incorporated into the $mystring variable. So if you typed perl ldir -rt /home/mydir, our line that runs the 'ls' system command would run "ls -l -rt /home/mydir", which it thinks is just fine. But what it won't do is give us the results from the '-l' flag if we pass that in because we weed all that out while extracting our directory names. We can fix this by explicitly checking if there are any flags passed in and hold onto those so that we can more intelligently display our results.
Back up at the top of our script, we can insert the following line after the "use strict;: statement:
my $l_flag = ($ARGV[0]) ? ($ARGV[0] =~ m/^-*l/) ? 1 : 0 : 0;


"WTF," you ask? We want to store a "1" or a "0" in $l_flag if we find there is a "-l" somewhere in the parameters passed into the program. We use a conditional operator (or turing operator) to determine this - actually we use one inside another one. To start, we look at the first element of @ARGV to see if there's anything there. If there is, we return whatever is immediately following the "?" and if there isn't we return what follows the " : ". But we'd like to know a little bit more than if there's just any old thing in that first element - it could be the directory name, it could be other kinds of flags - so we need to dig deeper. That's why we nest another conditional operator in the segment immediately following the first "?". The second conditional operator matches the first argument (now that we know it exists) to the regular expression "m/^-*l/" - which basically says "is the first character a dash and is there an "l" anywhere in there?". If there is, return a '1', if there isn't return a '0'. If you want to spread it out over multiple lines to make it more readable, you could write it like this:
($ARGV[0])
  ? ($ARGV[0] =~ m/-*l/)
    ? 1
    : 0
  : 0;



Then, we insert some conditional statements testing to see if the $l_flag variable was set when we print our output. If it was set, meaning we found a '-l' flag, just print the whole line that got from our 'ls -l' command, if not, use the weird regular expression to pull out only the name. Any other flags get applied regardless so there's nothing else to worry about.
foreach(@dirs){
 if(m/^d/){
   if($l_flag){
     print "$_\n";
   }
   else {
   $_=~m/\s(\S+$)/;
   print "$1\n";
   }
 }


And there you have it!

Here's the full code, with some lines combined:
#!/usr/bin/perl -w

use strict;

my $l_flag = ($ARGV[0]) ? ($ARGV[0]=~m/^-*l/) ? 1 : 0 : 0;

my $args=join(' ', @ARGV);

my @dirs=split('\n', my $mystring=`ls -l $args`);
foreach(@dirs){
 if(m/^d/){
   if($l_flag){
     print "$_\n";
   }
   else {
   $_=~m/\s(\S+$)/;
   print "$1\n";
   }
 }
} 



But wait! Hold on! If we want to use this as it is, we have to type "perl ldir blah blah blah" every time we go to execute it AND we have to be in the same directory as ldir!! That's lame! We want to be able to use it anywhere and only type "ldir blah blah blah" without the "perl" every time.

First, we need to make the program executable. To do this, type chmod 755 ldir. This makes us able to run the script without prefixing "perl" to it.

Next, we might want to change the user group to make sure that we can actually execute our script. Use chgrp users ldir to make it usable by the generic "users" group - or use another group if you want to make it a little more restrictive. In any case, just make sure that you're part of the group that is able to execute the program.

Next, we need to place the program somewhere in the system path so that the OS can find it when we type it's name. To do this, at the command line, type $PATH to see what directories are included in your system path. Find one that you have access to (or, if you have super user privileges and can place it anywhere you want, pick the first directory listed so that we ensure the script runs quickly). Then move the program to that directory. That should be it!

Now, where ever you are in your system, you can type ldir to see the list of directories in your current working directory, or you can type ldir some other directory to see the directories listed somewhere else. Best of all, it works just like 'ls' and you can use all the built in flags available to 'ls'!! So, if you want to see the directories in reverse order based on the times they were modified, including hidden directories, you can type ldir -rta to get that list.

Hope this was helpful and not too confusing!

Is This A Good Question/Topic? 2
  • +

Replies To: Create a custom Unix/Linux command

#2 Guest_Guest*


Reputation:

Posted 26 October 2010 - 06:17 AM

Nice, but you can just use:
ls -l |grep ^d

You can even create an alias like lsd = 'ls -l |grep ^d'
Was This Post Helpful? 0

#3 jumptrooper  Icon User is offline

  • D.I.C Head
  • member icon

Reputation: 68
  • View blog
  • Posts: 234
  • Joined: 19-August 10

Posted 28 October 2010 - 12:32 PM

View PostGuest, on 26 October 2010 - 05:17 AM, said:

Nice, but you can just use:
ls -l |grep ^d

You can even create an alias like lsd = 'ls -l |grep ^d'


very true! But it doesn't help you learn Perl.
Was This Post Helpful? 0
  • +
  • -

#4 moopet  Icon User is offline

  • binary decision maker
  • member icon

Reputation: 343
  • View blog
  • Posts: 1,189
  • Joined: 02-April 09

Posted 28 October 2010 - 01:02 PM

OT: Just going to point out that the backtick is only on the same key as the tilde on your country's keyboard layout. It moves around a fair bit.
Was This Post Helpful? 1
  • +
  • -

#5 jumptrooper  Icon User is offline

  • D.I.C Head
  • member icon

Reputation: 68
  • View blog
  • Posts: 234
  • Joined: 19-August 10

Posted 28 October 2010 - 01:44 PM

View Postmoopet, on 28 October 2010 - 12:02 PM, said:

OT: Just going to point out that the backtick is only on the same key as the tilde on your country's keyboard layout. It moves around a fair bit.


Thanks - you're right, I should definitely keep that in mind for the future.
Was This Post Helpful? 0
  • +
  • -

Page 1 of 1