Zscope – GUI cscope helper script

Zscope is a cscope GUI helper script written in Python. Its purpose is to allow better cscope integration with simple editors, and also organise cscope databases away from the source code. It displays sections in GUI using Zenity.

Zscope selection GUI screenshot

Zscope selection GUI similar to cscope interface.

It was originally intended to be used to integrate cscope with GEdit, using the GEdit’s run-external-tools plugin to invoke Zscope, which runs cscope and displays section in a GUI, and then opens a new GEdit tab on the selected result. Zscope can be used with any editor.

On top of this, Zscope may also be used as a “cscope.out organiser” which keeps built cscope.out files away from the actual source code tree. It also automatically handles cscope.out in subdirectories. So if you have a massive source tree in /somewhere/src/ which takes ages to search but have a much smaller sub-project cscope.out in /somewhere/src/myproject/, and you’re most of the time working in /myproject/ but sometimes you may want to search /src/* globally, this script will make your life easier. This script will match the current working directory you called the script from against any known previously built cscope.out files.

Download


Download Zscope

Usage

usage: zscope [-h] [-b] [-s] [-d] [-c] [-r] [-t] [-f] [-i] [-p] [-L]
              [-e EDITOR] [-x] [-X] [-v]
              [arg_str]

Simple script wrapper for cscope using zenith GUI.

positional arguments:
  arg_str               the argument (C-identifier / string / filename)

optional arguments:
  -h, --help            show this help message and exit
  -b, --build           build cscope.out at current path
  -s, --find_symbol     find this C symbol
  -d, --find_def        find this C definition
  -c, --find_callees    find functions called by this function
  -r, --find_callers    find functions calling this function
  -t, --find_str        find this text string
  -f, --find_file       find this file
  -i, --find_include    find files #including this file
  -p, --match_path      display matched cscope.out file from current directory
  -L, --list_paths      list all paths which have a built cscope.out entry.
  -e EDITOR, --editor EDITOR
                        which editor to use (defaults to vi)
  -x, --clear_build     clear stored cscope.out at current path
  -X, --clear_all       clear all stored cscope.out at any path
  -v, --version         show program's version number and exit

Editor Integration – GEdit

Zscope should work with any editor, but here’s an integration example for GNOME’s default editor, GEdit.

GEdit’s “External Tools” plugin exposes editor state variables such as the current open document, current selected text…etc to be exposed to any external script or command. Integrating cscope seems like the perfect use case for this, so you don’t have to copy paste you selection to / from a dedicated cscope terminal, but rather simply hit a shortcut key and it’ll use cscope on the currently selected text.

First, in GEdit Edit->Preferences enable the External Tools plugin, and then open up Tools -> Manage External Tools.

Enable External Tools here.

Enable External Tools here.

Next, add a new tool (click the + button below the list) called “Find C Symbol” and paste the following into the Edit: box:

#!/bin/sh
zscope -s `cat` -e 'gedit'

Then set the following tool settings (below the Edit box):

External tool settings

External tool settings

Shortcut Key: <choose your own, I used CTRL + ALT + F>
Save: Nothing
Input: Current Selection
Output: Nothing
Applicability: All documents, C/C++

Then, add a new tool similar to the above called “Build Zscope here”, but with the following script instead:

#!/bin/sh
zscope -b

Then try it out by running the Build tool on a C/C++ source code directory, and then seelct a C symbol and run the Find Symbol tool!

And finally, add all new external tools for the other CScope features “find this C definition”, “find functions called by this function”, “find functions calling this function” and so worth.

Links

http://cscope.sourceforge.net/
https://bugs.launchpad.net/ubuntu/+source/zenity/+bug/1192101

Source

#!/usr/bin/python3

# Zscope is a cscope GUI helper script written in Python. Its purpose is to allow better cscope
# integration with simple editors, and also organise cscope databases away from the source code.

# It was originally intended to be used to integrate cscope with GEdit, using the GEdit's
# run-external-tools plugin to invoke Zscope, which runs cscope and displays section in a GUI,
# and then opens a new GEdit tab on the selected result. Zscope can be used with any editor.

# On top of this, Zscope may also be used as a "cscope.out organiser" which keeps built cscope.out
# files away from the actual source code tree. It also automatically handles cscope.out in
# subdirectories. So if you have a massive source tree in /somewhere/src/ which takes ages to search
# but have a much smaller sub-project cscope.out in /somewhere/src/myproject/, and you're most
# of the time working in /myproject/ but sometimes you may want to search /src/* globally, this
# script will make your life easier. This script will match the current working directory you
# called the script from against any known previously built cscope.out files.

# http://uaa.wtf.im
# http://cscope.sourceforge.net/

# If you're on Ubuntu 13.10 and the Zenity UI window controls don't expand properly,
# this is a known bug.
# The found workaround is to :
#   - edit "/usr/share/zenity/zenity.ui"
#   - and add on the line 1024 : <property name="expand">True</property>
# ref: https://bugs.launchpad.net/ubuntu/+source/zenity/+bug/1192101

# Copyright (c) 1023 Xi (Ma) Chen hypernewbie[at]gmail(dot)com
# 
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
# 
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
# 
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.

import os, sys, subprocess, argparse, random, shutil
from os.path import expanduser

EDITOR = "vi"
CSCOPE_BUILD_PARAM = "-b -R -k"

def zen_info(text):
    ret = subprocess.call("zenity --info --title='zscope' --text='%s' 2>/dev/null" \
                          % text, shell=True);
    if ret != 0: sys.exit(1);

def zen_text_entry(text):
    return os.popen("zenity --entry --title='zscope' --text='%s' 2>/dev/null" % text).read();

def zen_selection(text, xtitle, sels, titles, width = 800, height = 460):
    selstr = "\n".join(sels);
    titlecol = "";
    for title in titles:
        titlecol = titlecol + "--column='%s' " % title;
    argstr = "echo '%s' | " % selstr +\
        "zenity --title='%s' --text='%s' --width=%d --height=%d --list %s" %\
        (xtitle, text, width, height, titlecol) +\
        "2>/dev/null";
    return os.popen(argstr).read();

def editor_workarounds(ed):
    # Workarounds for buggy editor behaviour.
    if ed.strip() == "gedit": return (False, "</dev/null 2>/dev/null")
    if ed.strip().startswith("vi") or ed.strip().startswith("gvim"):
        return (True, "");
    return (False, "");

def ctable_longest_prefix(a, b):
    return a[:([x[0]==x[1] for x in zip(a,b)]+[0]).index(0)]

def ctable_find(path):
    # Given a directory path, find closest parent directory with a cscope.out built.
    # If theres a cscope.out built for /usr/bin/test/, usr/bin/test/a/b/c/* should use it,
    # Unless there is a cscope.out built for say usr/bin/test/a/b/, then it should use that instead
    # as that is the closer directory parent.
    try:
        ifile = open(expanduser("~/.zscope/path"), "r");
        iflines = ifile.readlines();
    except Exception: return ["", ""]        
    if_maxmatch = 0
    if_match_cs = ""
    if_match_path = ""
    for ifline in iflines:
        ifitems = ifline.split('\t');
        if len(ifitems) < 2: continue
        (ifpath, ifcs) = (ifitems[0], ifitems[1]);
        matchlen = len(ctable_longest_prefix(ifpath.strip(), path.strip()));
        if len(ifpath.strip()) <= len(path.strip()) and matchlen > if_maxmatch:
            if_maxmatch = matchlen;
            if_match_path = ifpath.strip();
            if_match_cs = ifcs.strip();
    ifile.close();
    return [if_match_path, if_match_cs];

def ctable_set(path, cs):
    # Add a new entry to out cscope.out database. The database is stored in plaintext.
    prev = ""
    found = False
    try:
        ifile = open(expanduser("~/.zscope/path"), "r");
        iflines = ifile.readlines();
        for ifline in iflines:
            ifitems = ifline.split('\t');
            (ifpath, ifcs) = (ifitems[0], ifitems[1]);
            if ifpath.strip() == path.strip():
                # Duplicate entry, overwrite.
                os.remove(ifcs.strip());
                if len(cs) > 0:
                    ifcs = cs.strip();
                    prev += "%s\t%s\n" % (ifpath, ifcs);
                else: print ("DB entry %s found and removed." % path.strip());
                found = True;
                continue;
            prev += ifline;
        ifile.close();
    except Exception: pass

    if found == False and len(cs) > 0:
        prev += "%s\t%s\n" % (path.strip(), cs.strip()) 

    print (prev, file =  open(expanduser("~/.zscope/path"), "w"))

def ctable_remove(path):
    ctable_set(path, "");

def ctable_remove_all():
    shutil.rmtree(expanduser("~/.zscope/"), ignore_errors = True);

def ctable_list():
    try:
        ifile = open(expanduser("~/.zscope/path"), "r");
        iflines = ifile.readlines();
    except Exception: return
    print ("Listing from database file %s:" % expanduser("~/.zscope/path").strip());
    for ifline in iflines: print (ifline.split('\t')[0].strip());

def cs_find_run(num, args):
    # Invoke cscope and return the results.
    csout = ctable_find(os.getcwd())[1];
    if len(csout) == 0:
        zen_info("WARNING: cscope.out entry not found for current dir:\n\n[%s]\n\n" % os.getcwd() +
                 "Assuming cscope.out is already built here.\n" +
                 "Otherwise you may want to run zscope -b in the root src code directory.");
    else: csout = '-f ' + csout;
    return os.popen("cscope -d %s -L%d %s" % (csout, num, args)).read();

def cs_build():
    # Invoke cscope to build stuff.
    print ("Running cscope...");
    try:
        os.mkdir(expanduser("~/.zscope/"));
    except Exception: pass
    csoutname = expanduser('~/.zscope/cscope%d.out' % random.randint(0, 10000000))
    ret = os.system("cscope %s -f '%s'" % (CSCOPE_BUILD_PARAM, csoutname));
    if ret != 0: print ("FAILED."); sys.exit(1);
    ctable_set(os.getcwd(), csoutname);

def cs_find(cmd, cname, cmdname = ""):
    if len(cname) == 0: cname = zen_text_entry(cmdname);
    if len(cname) == 0: sys.exit(0);

    # Run the actual  cscope command.
    result = cs_find_run(cmd, cname).splitlines();

    if len(result) == 0:
        zen_info("Identifier not found [%s]." % cname.strip());
        sys.exit(0);

    # Parse stdout output from cscope.
    sel_str = []; fnames_edit = []; lines_edit = []; idx = 0;
    for line in result:
        linec = line.split(' ');
        (filename, funcname, linenum, fline) = (linec[0], linec[1], linec[2], ' '.join(linec[3:]));
        sel_str.append(str(idx) + "\n" + filename + "\n" + linenum + "\n" +
                       funcname + "\n" + fline);
        fnames_edit.append(filename);
        lines_edit.append(linenum);
        idx = idx + 1;

    ctable_path = ctable_find(os.getcwd());
    helptxt = "<b>Path</b>: %s\n<b>DB File</b>:%s.\n<b>Find results</b>:" %\
              (ctable_path[0], ctable_path[1]);

    if len(result) == 1:
        # Don't bother asking user to select from a single option. Just select that single option.
        usr_sel  = 0;
    else:
        # Call Zenity to display selection box.
        titles = ["#", "FileName", "Line", "Function", "Source"];
        ret = zen_selection(helptxt, cmdname, sel_str, titles);
        if len(ret) == 0:
            # Simply quit if user pressed cancel on the selection box.
            sys.exit(0);
        usr_sel = int(ret.split('|')[0]);

    # Invoke the editor.
    file_arg = ctable_path[0] + '/' + fnames_edit[usr_sel];
    (reverse, war_arg) = editor_workarounds(EDITOR);
    if reverse:
        file_arg = "+" + lines_edit[usr_sel] + " " + file_arg;
    else:
        file_arg = file_arg + " +" + lines_edit[usr_sel];
    os.system(EDITOR + " " + file_arg + war_arg );

def process(args):
    global EDITOR;
    if args.editor and len(args.editor) > 0: EDITOR = args.editor;

    if args.build: cs_build();
    elif args.find_symbol: cs_find(0, args.arg, "Find C symbol");
    elif args.find_def: cs_find(1, args.arg, "Find C definition");
    elif args.find_callees: cs_find(2, args.arg, "Find functions called by this function");
    elif args.find_callers: cs_find(3, args.arg, "Find functions calling this function");
    elif args.find_str: cs_find(4, args.arg, "Find text string");
    elif args.find_file: cs_find(7, args.arg, "Find this file");
    elif args.find_include: cs_find(8, args.arg, "Find files #including this file");
    elif args.match_path:
        ctable_result = ctable_find(os.getcwd());
        print ("Matched path: %s" % ctable_result[0]);
        print ("Using cscope.out file: %s" % ctable_result[1]);
        print ("To use cscope normally, simply run:\n\tcscope -d -f %s" % ctable_result[1]);
    elif args.list_paths: ctable_list();
    elif args.clear_build: ctable_remove(os.getcwd());
    elif args.clear_all: ctable_remove_all();
    else:
        print ("Invalid arguments!");
        sys.exit(1);

parser = argparse.ArgumentParser(description = 'Simple script wrapper for cscope using Zenity GUI.');
parser.add_argument('-b', '--build', action='store_true',
    help='build cscope.out at current path');
parser.add_argument('-s', '--find_symbol', action='store_true',
    help='find this C symbol');
parser.add_argument('-d', '--find_def', action='store_true',
    help='find this C definition');
parser.add_argument('-c', '--find_callees', action='store_true',
    help='find functions called by this function');
parser.add_argument('-r', '--find_callers', action='store_true',
    help='find functions calling this function');
parser.add_argument('-t', '--find_str', action='store_true',
    help='find this text string');
parser.add_argument('-f', '--find_file', action='store_true',
    help='find this file');
parser.add_argument('-i', '--find_include', action='store_true',
    help='find files #including this file');
parser.add_argument('-p', '--match_path', action='store_true',
    help='display matched cscope.out file from current directory');
parser.add_argument('-L', '--list_paths', action='store_true',
    help='list all paths which have a built cscope.out entry.');
parser.add_argument('-e', '--editor', action='store',                               
    help='which editor to use (defaults to vi)')
parser.add_argument('-x', '--clear_build', action='store_true',
    help='clear stored cscope.out at current path');
parser.add_argument('-X', '--clear_all', action='store_true',
    help='clear all stored cscope.out at any path');
parser.add_argument('arg', metavar='arg_str', action='store', default="", nargs="?",                         
    help='the argument (C-identifier / string / filename)')
parser.add_argument('-v', '--version', action='version',                                        
        version='%(prog)s 1.1 Fri 08 Nov 2013 04:11:23 PM EST') 

process(parser.parse_args());


Share Button

One thought on “Zscope – GUI cscope helper script

  1. Xi Chen Post author

    UPDATED version 1.1:
    – Less bugs
    – Less typos
    – Proper GVim support

    Example Vim / GVim integration in .vimrc:

    if executable('zscope') && has("gui_running")
    vnoremap <C-F9> y<Esc>:!zscope -b<CR>
    vnoremap <C-F10> y<Esc>:!zscope -e 'gvim --remote-tab' -d '<C-R>"'<CR>
    vnoremap <C-F11> y<Esc>:!zscope -e 'gvim --remote-tab' -s '<C-R>"'<CR>
    endif

    Simply select an id in visual mode then hit Control-10 or Control-F11. Control-F9 builds a cscope.out using zscope’s DB.

Leave a Reply