Wednesday, May 21, 2008

Life with Cygwin: Paths, Notepad and Clipboard

I have in the past written about my Cygwin setup, but several weeks ago, while I was in Scotts Valley and having dinner with Adam Markowitz and Steve Trefethen, Steve mentioned that I should write a bit more about my setup.

While the defaults for a new Cygwin install today are better than they have ever been, there are still a lot of things to be desired. Using, as I do, a bash shell as my main command line, yet still being a Windows programmer running on Windows, means that I need to integrate with Windows command-line programs. Herein lies a problem: Cygwin uses Unix-like paths with '/' and no drive letter or colon (which is a path separator on Unix systems), while Windows inherits the usual CP/M/DOS traditions. Incidentally, I mount those drives to root letters to make converting between Windows and Cygwin paths easy:

$ mkdir /c
$ mount 'c:\' /c

Mounting removable drives like floppy disks and DVD-drives in the same way is more problematic, as the 'ls --color=auto' command (which wants to colour in files and directories corresponding to thier types) will try to read the contents of these directories, which of course will be mounted to removable drives. This would normally cause delays when doing a listing of the root directory, as the removable drives in the system spin up etc. Consequently, for removable drives I use a different technique. For example, my DVD-drive is 'O:' (because it's round, and because I frequently add and remove drives and I don't like drive letters changing because it breaks things), so this is how I integrate the DVD-drive into the Cygwin file system:

$ ln -s '/cygdrive/o' /o

This creates a symbolic link which will just be a broken link when there is no DVD in the drive. I do similar things for my iPods, pen drives, floppy drive (I still keep one around, just in case :), etc.

Anyway, back to Cygwin/Windows path interaction. Cygwin does provide a command to convert between paths, called 'cygpath'. It can be used fairly easily in an ad-hoc way on the command-line:

$ notepad /etc/bash.bashrc
# (this will fail, as notepad can't cope)

$ notepad $(cygpath -w /etc/bash.bashrc)
# (this will work here but fails when the Windows path has spaces)

$ notepad "$(cygpath -w /etc/bash.bashrc)"
# (this is more resilient)

winexec

Using cygpath manually is a bit of a pain, so I wrote a little bash script I call winexec to capture the pattern:

#!/bin/bash

function usage
{
    echo "usage: $(basename $0) [options]  [...]"
    echo "Executes an executable with arguments, converting non-options into Win32 paths."
    echo "Options:"
    echo "  -f    Only convert paths to files or directories which actually exist."
    echo "  -s    Use cygstart to execute detached from console."
    echo "  -k    Skip converting paths until '**' found in arguments (and remove the '**')."
    echo "  --    Terminate $(basename $0) options processing."
    exit 1
}

# Process options to winexec itself.

while [ "$1" ]; do
    case "$1" in
        -f)
            ONLY_FILES=1
            ;;
        
        -s)
            USE_CYGSTART=1
            ;;
        
        -k)
            SKIP_TO_STAR=1
            ;;
        
        --)
            shift
            break
            ;;
        
        -*)
            # Give an error on unknown switches for future compat.
            usage
            ;;
        
        *)
            break
            ;;
    esac
    shift
done

EXECUTABLE="$1"
shift

test -z "$EXECUTABLE" && usage

# Options conversion and caching.

declare -a OPTS
function add_opt
{
    OPTS[${#OPTS[@]}]="$1"
}

function add_file_opt
{
    if [ -n "$SKIP_TO_STAR" ]; then
        if [ "$1" = "**" ]; then
            SKIP_TO_STAR=
            # Eat '**' but don't add.
        else
            # Haven't seen star yet, so add unconverted.
            add_opt "$1"
        fi
    else
        if [ -n "$ONLY_FILES" ]; then
            if [ -f "$1" -o -d "$1" ]; then
                add_opt "$(cygpath -w "$1")"
            else
                add_opt "$1"
            fi
        else
            add_opt "$(cygpath -w "$1")"
        fi
    fi
}

# Process arguments to executable.

while [ "$1" ]; do
    case "$1" in
        -*)
            add_opt "$1"
            ;;
        
        *)
            add_file_opt "$1"
            ;;
    esac
    shift
done

# Actually start the executable.

if [ "$USE_CYGSTART" ]; then
    cygstart -- "$EXECUTABLE" "${OPTS[@]}"
else
    "$EXECUTABLE" "${OPTS[@]}"
fi

For an example of how I use that, I have another script called 'dir', for when I feel like I need classic 'dir' options:

#!/bin/bash

winexec -f -k cmd /c dir '**' "$@"

All these scripts, BTW, go in my ~/bin directory and are chmod'd 0755 to make them executable:

$ mkdir ~/bin
$ chmod -R 0755 ~/bin/*

My system's PATH (i.e. the Windows PATH, from System Properties | Advanced | Environment Variables) includes my home directory's bin directory before the Cygwin bin directories, but it also includes those. There can be some knots here though, which I won't get into today. The scripts also need to use Unix line-endings, though Cygwin was less strict about this in the past. It's easily enough done, though: the dos2unix command will normalize to Unix any text files given as arguments.

n

Notepad is a classic programmer's tool - as in "all I need is Notepad and the compiler" (or maybe just Notepad ;), etc. Since Notepad doesn't react so well to multiple file arguments, it isn't completely suitable to the winexec trick. I have a customized script for Notepad:

#!/bin/bash

if [ -z "$1" ]; then
    echo "usage: $(basename $0) ..."
    echo "Starts notepad on the file(s)."
    echo "If  is -, then standard input is redirected to a temp file and opened."
    exit 1
fi

for file in "$@"; do
    if [ "$file" = "-" ]; then
        file=$(mktemp)
        cat '-' > $file
        (
            notepad "$(cygpath -w "$file")"
            rm $file
        ) &
    else
        cygstart -- notepad "$(cygpath -w "$file")"
    fi
done

Having created this little utility, I can open multiple files in notepad just using the bash wildcards:

$ n /c/windows/*.txt
# (there aren't too many of these guys)

Similarly, I can capture a program's output into Notepad for reference in a separate window and possible printing:

$ dir /c | n -
# (opens a notepad window containing the directory listing for C:\)

Copy and Paste

Finally (for now), good Windows integration requires good clipboard integration. The native-Windows rxvt terminal which ships with Cygwin already support automatic copy on selection and paste with middle-cilck or Shift+Ins, familiar to Unix console and X users. However, I often want to copy the output of a command to the clipboard, or get a copied piece of text into a file, or transform the contents of the clipboard (perhaps to do a search and replace on it), etc. Thus, I wrote two little utilities in Delphi, copy-clipboard.dpr and paste-clipboard.dpr:

Copy

{$APPTYPE CONSOLE}

uses
  SysUtils, Classes, Clipbrd;

var
  list: TStringList;
  line: string;
begin
  try
    list := TStringList.Create;
    while not Eof(Input) do
    begin
      Readln(line);
      list.Add(line);
    end;
    Clipboard.AsText := list.Text;
  except
    on e: Exception do
      Writeln(ErrOutput, e.Message);
  end;
end.

(Freeing objects that have no external effect when freed before you're about to exit the program is the height of pointlessness, in case you were wondering.)

Paste

{$APPTYPE CONSOLE}

uses
  SysUtils, Clipbrd;

begin
  Write(Clipboard.AsText);
end.

These two utilities, having been compiled, renamed to c.exe and p.exe, and moved to my ~/bin directory, come in very handy. For example, should I myself have wanted to copy one of the above scripts, I normally just select and copy script text, and:

$ p > ~/bin/winexec
$ chmod 0755 ~/bin/winexec

Similarly, I sometimes want to search and replace on text on the clipboard:

$ p
Similarly, I sometimes want to search and replace on text on the clipboard:
# (showing what's on the clipboard)
$ p | sed 's| |_|g' | c
# (replace all spaces with underscores)
$ p
Similarly,_I_sometimes_want_to_search_and_replace_on_text_on_the_clipboard:

A not usually unwelcome side-effect of my clipboard commands is that they normalize line endings and add a newline sequence at the end of the text, if there isn't one already.

I hope I've given a few folk some ideas about optimizing their environment, particularly if they're command-line junkies like me.

7 comments:

Steve Trefethen said...

Now that's what I'm talkin' 'bout!

Thanks man, it's always good to see how others get it done.

mr.markowitz said...

You command line junkie you!! Good stuff!

;)

-A

m. Th. said...

1. You where in Scotts Valley? Where is your blog with the impressions? :-)

2. Perhaps is better to change the notepad with Notepad++? See http://notepad-plus.sourceforge.net/uk/about.php

Barry Kelly said...

@m.th.: I primarily use Notepad in order to have a window open on the desktop showing some text, not as an editor. For ad-hoc editing, I use Joe running on Cygwin. Meanwhile, Notepad++ is an MDI app and doesn't use the Windows "Window Background Color" by default, as well as being slower to start up and using more memory.

m. Th. said...

Ah, I see... Just trying to help... :-)

So, you owe us only a blog with the impressions. :-)

Meanwhile, in the .non-tech is a bunch of wish-lists. Also, in the yesterday event (The online chat with Wayne Williams, EMBT's CEO - an /awesome/ event and a nice guy to talk - many many delphians out there) where some very neat questions/wishes to him, and in parallel (in the Chat room) Nick was assaulted in the same manner. In order to enroll in the community's 'fashion' ;-), can you share with us your top 5-10 wishes from your POV?

Anonymous said...

Thanks for sharing your great experience. A little comment on copy and paste: Cygwin has provided two cmds to communicate with clipbroad, getclip.exe and putclip.exe

Tallak Tveide said...

cat file | /dev/clipboard
cat /dev/clipboard | less

;-)