Scripting the Linux desktop, Part 2
Using Python to expand and extend Nautilus
This content is part # of # in the series: Scripting the Linux desktop, Part 2
This content is part of the series:Scripting the Linux desktop, Part 2
Stay tuned for additional content in this series.
For users of the GNOME desktop, the Nautilus program is probably one of the more frequently used applications. It handles all the file copying, moving, renaming, and searching chores with a simple graphical interface. At first blush, it would appear there aren't many file-related things Nautilus can't do—unless you start thinking about tasks you would typically perform with a shell script.
The Nautilus developers provided several ways to add new functionality without breaking open the main code base. The simplest method is to use a bash or shell script that executes a series of commands you would usually perform from a terminal prompt. This method makes it possible to try the commands to make sure they do what you want them to do first. You can use other languages as well, including the C Scripting Language, GnomeBasic, Perl, and Python. This article looks at adding new capabilities to Nautilus using the Python language. A basic understanding of the Python language and the Python Standard Library is assumed.
The first method for extending Nautilus is through a special directory found in /home named .gnome2/nautilus-scripts. Any executable file placed in this directory will appear when you right-click a file or folder under the Scripts menu. You can also select multiple files or folders and pass a list of files to a script with the same right-click method.
Nautilus makes available a number of environment variables containing things like the current directory and the selected files when a script is invoked. Table 1 shows these environment variables.
Table 1. Nautilus environment variables
|NAUTILUS_SCRIPT_SELECTED_FILE_PATHS||Newline-delimited paths for selected files (only if local)|
|NAUTILUS_SCRIPT_SELECTED_URIS||Newline-delimited URIs for selected files|
|NAUTILUS_SCRIPT_CURRENT_URI||The current location|
|NAUTILUS_SCRIPT_WINDOW_GEOMETRY||The position and size of the current window|
In Python, you obtain the value of these variables with a single call to the
os.environ.get function as follows:
selected = os.environ.get('NAUTILUS_SCRIPT_SELECTED_FILE_PATHS,'')
This call returns a string with paths to all the selected files delimited with the newline character. Python makes it easy to turn this string into an iterable list with the following line:
targets = selected.splitlines()
At this point, it's probably a good idea to stop and talk about user interaction. Once control is passed from Nautilus to the script, there are really no restrictions on what the script does from that point on. Depending on what the script does, there might not even be a need for any user feedback, with the exception of some type of completion or error message, which you can take care of with a simple message box. Because Nautilus is written using the gtk windowing toolkit, it seems like a logical choice to do the same, although this is not required. You could just as easily use TkInter or wxPython.
For the purposes of this article, you'll use gtk. Producing a simple message box to communicate completion status requires just a few lines of code. For readability purposes, this code will fit best if you create a simple function to generate the message. Doing so requires a total of four lines of code:
def alert(msg): dialog = gtk.MessageDialog() dialog.set_markup(msg) dialog.run()
Example: Creating a simple script to return the number of selected files
The first example program combines these snippets into a simple script that
returns the number of files currently selected. This script will work for
individual files or directories. You'll use another Python library function,
os.walk, to recursively build a list of files in
each directory. A total of 38 lines of code, shown in Listing 1,
is all you needed for this little utility, including blanks lines.
Listing 1. Python code for the Filecount script
#!/usr/bin/env python import pygtk pygtk.require('2.0') import gtk import os def alert(msg): """Show a dialog with a simple message.""" dialog = gtk.MessageDialog() dialog.set_markup(msg) dialog.run() def main(): selected = os.environ.get('NAUTILUS_SCRIPT_SELECTED_URIS', '') curdir = os.environ.get('NAUTILUS_SCRIPT_CURRENT_URI', os.curdir) if selected: targets = selected.splitlines() else: targets = [curdir] files =  directories =  for target in targets: if target.startswith('file:///'): target = target[7:] for dirname, dirnames, filenames in os.walk(target): for dirname in dirnames: directories.append(dirname) for filename in filenames: files.append(filename) alert('%s directories and %s files' % (len(directories),len(files))) if __name__ == "__main__": main()
Figure 1 shows what you should see in Nautilus when you right-click a file or select a group of files. The Scripts menu option displays all executable files in .gnome2/nautilus-scripts and also gives you the option to open that folder. Selecting one of the files executes that script.
Figure 1. Selecting files in Nautilus
Figure 2 shows the output from running the Filecount.py script.
Figure 2. Filecount.py output
A few things might come in handy as you go about debugging your Nautilus scripts. The first thing is to close all instances of Nautilus to allow it to reload completely and find your new scripts or extensions. You can do so with the command:
The next handy command allows you to run Nautilus without opening your preference or profile data. This could save you a few steps later on in the event your script or extension inadvertently corrupts something. Here's the command:
The only step remaining to make your
accessible from Nautilus is to copy it to the ~/.gnome2/nautilus-scripts
directory and change the file mode to allow execution. You do so with the
chmod +x Filecount.py
Example: Creating a file cleanup utility
As a second example, create a file cleanup utility to look for any files that
might be considered temporary generated by editors such as Vim or EMACS.
The same concepts could be used to purge a directory of any specific files
by simply modifying the
check function. This
code falls into the silent operation category, meaning it executes
and provides no feedback to the user.
The main function of this script looks basically the same as the previous example
with a few minor exceptions. This code uses the concept of recursion to
call the main function multiple times until the last directory has been
traversed. You could use the
os.walk function to
accomplish the same task without using recursion. File checking occurs in the
check function and simply looks for files ending
with a tilde (
~) or pound sign (
beginning with a pound sign, or ending with the extension
This example shows off the extensive number of functions the Python Standard
os module provides. It's also a good
example of an operating system-independent way of manipulating path
names and directories as well as performing file operations.
Listing 2 shows the code for this script.
Listing 2. Python code for the cleanup script
#!/usr/bin/env python import pygtk pygtk.require('2.0') import gtk import os def check(path): """Returns true to indicate a file should be removed.""" if path.endswith('~'): return True if path.startswith('#') and basename.endswith('#'): return True if path.endswith('.pyc'): return True return False def walk(dirname=None): selected = os.environ.get('NAUTILUS_SCRIPT_SELECTED_FILE_PATHS', '') curdir = os.environ.get('NAUTILUS_SCRIPT_CURRENT_URI', os.curdir) if dirname is not None: targets = [dirname] elif selected: targets = selected.splitlines() else: targets = [curdir] for target in targets: if target.startswith('file:///'): target = target[7:] if not os.path.isdir(target): continue for dirname, dirnames, files in os.walk(target): for dir in dirnames: dir = os.path.join(dirname, dir) walk(dir) for file in files: file = os.path.join(dirname, file) if check(file): os.remove(file) if __name__ == '__main__': walk()
The second method for enhancing Nautilus is through the creation of extensions. This method is somewhat more complex than the first but brings added benefits. Nautilus extensions can be embedded in the file display window, so you can write an extension that will populate a column with information previously unavailable. One of the first things you may need to do is install the python-nautilus extensions with the following command:
sudo apt-get install python-nautilus
This command downloads and installs the necessary files, including documentation and examples. You can find the sample code in the directory /usr/share/doc/python-nautilus/examples. Once installed, you have access to a set of Nautilus classes and providers to program against. Table 2 shows the list.
Table 2. Nautilus classes and providers
|Class or provider||Description|
|Reference to the Nautilus |
|Reference to the Nautilus |
|Reference to the Nautilus |
|Reference to the Nautilus |
|Reference to the Nautilus |
|Allows output to be displayed in a Nautilus column|
|Provides information about the file|
|Displays the location|
|Adds new functionality to the right-click menu|
|Adds information to the property page|
The examples provided on the gnome.org site demonstrate the use of
MenuProvider (background-image.py and
InfoProvider (block-size-column.py), and
PropertyPageProvider (md5sum-property-page.py). The
ColumnProvider uses 13 lines of executable Python
code to introduce a new column to Nautilus. Once this code has been placed in
the proper directory (~/.nautilus/python-extensions) and Nautilus has restarted,
you should see a new option when you click View > Visible Columns.
The Visible Columns option only appears when you have set your
view type to List. Enabling the Block size
column by selecting the check box displays the results of the following Python
The basic pattern of any Python extension is to subclass the existing Nautilus
provider base class, and then execute a series of instructions that will eventually
return the appropriate Nautilus object. In the block-size-column.py example, the
object returned is
nautilus.Column. You must pass four
parameters back to Nautilus, including
description. The Python code for this example is:
return nautilus.Column("NautilusPython::block_size_column", "block_size", "Block size", "Get the block size")
Coding a new extension involves inheriting needed information from the specified
base classes. In the block-size-column.py example, both
nautilus.InfoProvider are enumerated in the class
definition, so the new class inherits from both. The next thing you have to do is
override any method from the base class or classes to populate the column. You
do so in the block-size-column.py example by overriding the
Passing information to a Nautilus extension works differently than the scripting case.
Nautilus actually launches a new process to execute a script and sets a number of
environment variables to pass information. Extensions execute in the same process
as Nautilus and, therefore, have access to objects, methods, and attributes.
Information about a file gets passed through the
object, including things like
add information to the
FileInfo object, you must call
add_string_attribute method. You'll use this
approach in the example that follows to add new attributes to the
Example: Show the number of lines in a file
The first example uses the
method to show the number of lines and characters when you right-click
a file (or files) and then click Properties. The basic idea
behind this extension is to count the number of lines and characters in a
file and report the results on a new tab of the file properties page.
Extensions have direct access to Nautilus data structures, including the
file object. The only thing you have to do is
unpack the name using the
function as follows:
filename = urllib.unquote(file.get_uri()[7:]
A few lines of Python code do the main work of counting the number of lines
and characters. For this example, you create a
function to read the entire file into one big string, and then count the
total number of characters and the number of newline characters. Because
the property page can be displayed for a number of selected files and
directories, you have to make provisions for counting multiple files. All
that's left at this point is to add the results to a new page on the property
page. This example creates a simple
and then populates a number of labels with the information gathered, as
Listing 3 shows.
Listing 3. The Linecountextension.py file
import nautilus import urllib import gtk import os types = ['.py','.js','.html','.css','.txt','.rst','.cgi'] exceptions = ('MochiKit.js',) class LineCountPropertyPage(nautilus.PropertyPageProvider): def __init__(self): pass def count(self, filename): s = open(filename).read() return s.count('\n'), len(s) def get_property_pages(self, files): if not len(files): return lines = 0 chars = 0 for file in files: if not file.is_directory(): result = self.count(urllib.unquote(file.get_uri()[7:])) lines += result chars += result self.property_label = gtk.Label('Linecount') self.property_label.show() self.hbox = gtk.HBox(0, False) self.hbox.show() label = gtk.Label('Lines:') label.show() self.hbox.pack_start(label) self.value_label = gtk.Label() self.hbox.pack_start(self.value_label) self.value_label.set_text(str(lines)) self.value_label.show() self.chars_label = gtk.Label('Characters:') self.chars_label.show() self.hbox.pack_start(self.chars_label) self.chars_value = gtk.Label() self.hbox.pack_start(self.chars_value) self.chars_value.set_text(str(chars)) self.chars_value.show() return nautilus.PropertyPage("NautilusPython::linecount", self.property_label, self.hbox),
Figure 3 shows the result of right-clicking a file and clicking the Linecount tab. It's important to note at this point that this feature works on individual files or any group of selected files and directories. The reported number will represent all lines in all files.
Figure 3. Clicking the Linecount tab to view the number of lines in a file
Finally, alter your extension utility to populate a column instead of a property
page. The modifications to the code are fairly minor, although you do need to
inherit from both
nautilus.InfoProvider. You also have to implement
get_columns method simply returns the
information gathered by the
count method uses a different technique for the
column provider extension. Python's
is used to read all lines in a file into a list of strings. Counting the total number
of lines is simply the number of elements in the list returned with the
len(s) statement. Not that the file type check
is common to both examples: It only makes sense to
count text files that actually have lines to count. You create a list of
acceptable file extensions with the line:
types = ['.py','.js','.html','.css','.txt','.rst','.cgi']
A second list contains exceptions that won't be counted. For this example, exclude a single file with the line:
exceptions = ['MochiKit.js']
These two lists are then used to include or exclude files with the following two lines of code:
if ext not in types or basename in exceptions: return 0
The entire extension requires a total of 26 lines of executable code. You'll want to modify the exceptions and types lists to include or exclude the files of interest to you. Listing 4 shows the completed extension.
Listing 4. Python code for the Linecountcolumn extension
import nautilus import urllib import os types = ['.py','.js','.html','.css','.txt','.rst','.cgi'] exceptions = ['MochiKit.js'] class LineCountExtension(nautilus.ColumnProvider, nautilus.InfoProvider): def __init__(self): pass def count(self, filename): ext = os.path.splitext(filename) basename = os.path.basename(filename) if ext not in types or basename in exceptions: return 0 s = open(filename).readlines() return len(s) def get_columns(self): return nautilus.Column("NautilusPython::linecount", "linecount", "Line Count", "The number of lines of code"), def update_file_info(self, file): if file.is_directory(): lines = 'n/a' else: lines = self.count(urllib.unquote(file.get_uri()[7:])) file.add_string_attribute('linecount', str(lines))
Figure 4 shows a Nautilus window with the Line Count column enabled. Each individual file will have the total number of lines displayed. You'll have to do some math using this method should you need to get a total of multiple files.
Figure 4. The Line Count column in a Nautilus window
Extending Nautilus with Python is really a straightforward process. The beauty and elegance of Python and the Python Standard Library make for both efficient and readable code. Navigating the documentation and examples on the gnome.org site can be challenging but not impossible. A few Google searches will turn up additional examples, as well. The examples here will hopefully give you ideas of how to extend Nautilus in ways that will meet specific needs. If you're familiar with coding in Python, you shouldn't have any problems.
- Learn more about Nautilus.
- Find more Python resources at Python.org.
- In the developerWorks Linux zone, find hundreds of how-to articles and tutorials, as well as downloads, discussion forums, and a wealth of other resources for Linux developers and administrators.
- Evaluate IBM products in the way that suits you best: Download a product trial, try a product online, use a product in a cloud environment, or spend a few hours in the SOA Sandbox learning how to implement Service Oriented Architecture efficiently.
- Follow developerWorks on Twitter, or subscribe to a feed of Linux tweets on developerWorks.