Scheme line "values"

Years ago, when I first saw TextMate demonstrated, one of the ways it was used as a teaching tool, when teaching Ruby, was to have the current line executed, and the value it returned appended to the current line:

(2 + 3) * 4 / 5 # => 4

That is, pressing Cmd-Shift-Ctrl-E would execute the line, and update the marker.

Today, while playing around with Scheme, I came up with a neat way to do the same type of thing.

Initially, I made it so that it executed the current line, and added/updated the marker. Then, I realised I could load the file, and then execute the current line.

You can create a new bundle command, and bind it to whatever key you want, with a scope selector of source.scheme, Input of Line, Output of Replace Input.

#!/usr/bin/env bash

[[ -f "${TM_SUPPORT_PATH}/lib/" ]] && . "${TM_SUPPORT_PATH}/lib/"

# Evaluate the current line in our Scheme interpreter
# The interpreter you use should be set in the environment
# variable TM_SCHEME

# The whole file will be loaded, and the current line's value executed,
# and added to the line as a comment.

CMD=$(basename "$INTERPRET")

LINE=`cat /dev/stdin | sed 's/; =>.*//'`
VALUE=`echo $LINE | $INTERPRET --load $TM_FILEPATH | grep ';Value: ' | sed 's/;Value: //'`

echo -n $LINE "; =>" $VALUE

Unfortunately, trailing comments are handled as a seperate line, so getting the ruby-like behaviour of updating all of the ; => comments will have to wait for another day.

arp -a | vendor

I have lots of things on my local network. Most of them behave nicely with the DHCP server, and provide their machine name as part of their DHCP request (Client ID), which means I can see them in the list in Airport Utility.

However, some of them don’t which means I have some blank rows.

It would be nice to be able to figure out which devices these are, especially for those that don’t provide any services (particulary a web interface).

Enter MAC Vendor Lookup.

You can register, and get an API key that will return values in the format you desire.

Then, it’s possible to do:

$ curl --silent | cut -f 1 -d \|

(I use the pipe delimited version).

This is all well and good, but who wants to have to type them in? Not this guy.

Let’s look at how we can get them from arp -a.

$ arp -a | cut -f 4 -d ' '

Okay, that’s promising, it gives me a list of MAC addreses. Almost. It skips out leading zeros, which the API rejects. And it includes ones that are missing.

Cue about an hour mucking about with the (limited) sed regex docs:

$ arp -a | 
    cut -f 4 -d ' ' | 
    sed -E 's/:([[:xdigit:]]):/:0\1:/g' | 
    sed -E 's/^.:/0&/' | 
    sed -E 's/:(.)$/:0\1/'

Ah, that’s better. Now we have the proper MAC addresses.

Now, we can pipe this information through the API call.

This is where we need to start to get a bit tricky. We need to create a function that will allow us to call the API with a new value each time. You’ll want to stick this in your .bashrc.

function mac_vendor() {
  $API_KEY="<your api key>"
  if [[ $1 ]]; then
    curl --silent "$API_KEY/:API_KEY/$1" | cut -f 1 -d \|
    while read DATA; do
      curl --silent "$API_KEY/:API_KEY/$DATA" | cut -f 1 -d \|

The if statement means we can use it by passing an argument on the command line:

$ mac_vendor 00:00:00:00:00:00
Xerox Corporation

Or by passing through data from stdin:

$ arp -a | 
    cut -f 4 -d ' ' | 
    sed -E 's/:([[:xdigit:]]):/:0\1:/g' | 
    sed -E 's/^.:/0&/' | 
    sed -E 's/:(.)$/:0\1/' |

Okay, that’s nice, but we now can’t see which IP address is associated with which vendor.

Let’s move that ugly chained sed call into it’s own function, called normalise_mac_address, which we will also wrap in a while read DATA; do ... done clause, so we can pipe data through it:

function normalise_mac_address() {
  while read DATA; do
    echo $DATA |
      sed -E 's/:([[:xdigit:]]):/:0\1:/g' |
      sed -E 's/^.:/0&/' |
      sed -E 's/:(.)$/:0\1/'

Nearly there!

We now need to be able to grab out the IP address and the MAC address from arp, and pass only the MAC address through our conversion functions. By default the bash for … in … construct will iterate through words, so we need to tell it to deal with a line at a time:

function get_all_local_vendors() {
  for LINE in `arp -a | cut -f 2,4 -d ' '`; do
    # We have LINE="(<>) <mac:address:here>"
    MAC=`echo $LINE | cut -f 2 -d ' ' | normalise_mac_address`
    IP=`echo $LINE | cut -f 1 -d ' '`
    # We only want ones that were still active
    if [ $MAC != '(incomplete)' ]; then
      VENDOR=`echo $MAC | mac_vendor`
      echo $VENDOR $IP

I’m hardly a bash expert, so there may be a better way of doing things rather than the repeated VARIABLE=`foo thing` construct I keep using.

So, the outcome I get when I run this looks something like:

$ get_all_local_vendors 
Apple, Inc. (
Sparklan Communications, Inc. (
Devicescape Software, Inc. (
Mitrastar Technology (
Apple, Inc. (
Silicondust Engineering Ltd (
Apple Computer (
none (

Getting rid of that last one is left as an exercise to the reader: the MAC address is FF:FF:FF:FF:FF:FF.

Am I connected remotely?

I now share my dotfiles between the various OS X machines I use daily, using Dropbox and symlinks.

However, I have many aliases and functions that need to act differently if I only have a console session at the machine in question, or a full GUI session.

With bash, this is easy to test:

1 export EDITOR='nano'
2 if [[ -z "$SSH_CONNECTION" && $OSTYPE =~ ^darwin ]]; then
3 export EDITOR='mate --wait'
4 export TEXEDIT='mate -w -l %d "%s"'
5 export LESSEDIT='mate -l %lm %f'
6 fi

Now, if I am remotely connected to a machine, then I will get nano as my editor, but if I am sitting directly in front of it, then it will open Textmate.

View man pages in Preview

It’s not a new concept, but here is my take on it:

 1 function man {
 2     # We can get the actual path to the man command here, so we can override
 3     # it with our function name.
 4     MAN=`which man`
 5     # Change these two if you are not on OS X.
 6     CACHE_DIR="${HOME}/Library/Caches/manpages"
 7     OPEN="open"
 9     # If we don't have any arguments, use the nice man error message
10     if [ ! $1 ]; then
11         $MAN
12         return
13     fi
15     # If we have an argument that clashes with what we are wanting to be
16     # able to do, pass the whole command through.
17     for ARG in $*; do
18         case $ARG in 
19             -[dfkKwtWP])
20                 $MAN $*
21                 return;;
22         esac
23     done
25     # Make sure our cache directory exists.
26     mkdir -p $CACHE_DIR
27     # Get the man page(s) that match our query.
28     MAN_FILES=`$MAN -w $*`
29     for MAN_FILE in $MAN_FILES; do
30         # Get the name of the man file, and the section.
31         MAN_PAGE=`basename "$MAN_FILE" | cut -d \. -f 1-2 | sed 's/\./(/' | sed 's/$/)/'`
32         # Our PDF will be in this location
33         PDF_FILE="${CACHE_DIR}/${MAN_PAGE}"
35         # If we actually have a man file that matches
36         if [ -n "$MAN_FILE" ]; then
37             # See if the man file is newer than our cached PDF, and if it is,
38             # then generate a new PDF. This works even if $PDF_FILE does not
39             # exist.
40             if [ $MAN_FILE -nt $PDF_FILE ]; then
41                 $MAN -t $* | pstopdf -i -o "$PDF_FILE"
42             fi
43             # Then display the file.
44             $OPEN "$PDF_FILE"
45         fi
46     done
47 }

Tab completion and ssh/open -a

I use the Terminal just as much as the Finder, and have tab-completion turned on in bash. To make it better, you can set it so that it will complete differently depending upon what you have already typed in.

The first one of these tips will autocomplete from the ~/.ssh/known_hosts file, so that when you type in:

$ ssh ma[tab]

1 complete -W "$(echo `cat ~/.ssh/known_hosts | cut -f 1 -d ' ' | sed -e s/,.*//g | uniq | grep -v "\["`;)" ssh

it will autocomplete the servers you ssh to that start with “ma”.

The next one is more complicated - it allows you to complete from all available applications when typing:

$ open -a [tab]

1 complete -W "$(/bin/lsregister -dump | /usr/bin/sed -E -n -e '/\/Applications/{s/^.+ ((\/Applications|\/Developer).+\.app)$/\1/p;}' | \/usr/bin/sed 's/ /\ /g' | \/usr/bin/sed -e s/\'/\\\'/g | /usr/bin/xargs /usr/bin/basename -s '.app' | /usr/bin/sed 's/ /\\\ /g')" open -a

These can be added to one of your bash startup files: mine live in ~/.bashrc.

Changing UNIX shell without actually changing it

Very rarely, I encounter a computer system I have to use regularly, but I don’t have superuser status on. Notably, at Uni, I have access to a SunOS system, where I actually have to use it from time to time. Most of the time this is just via ssh, but sometimes it’s a physical login to a SunRay workstation.

I much prefer bash over other shells, not because it is necessarily better, but that it is just the one I use most of the time. I’ve got some nice systems to help me out, like using a different colour for the user@host string on each machine, so I can easily see which machine the current ssh session is actually logged into.

However, at Uni there are lots of restrictions. We can run /usr/bin/bash, but we can’t change our default shell to it. In fact, we can’t change our default shell at all, which is kinda dumb. I’ve tried all sorts of tricks, but I just can’t do it.

The next step is to have your .login, or whatever, run the shell you want. For me, this is safe-ish, since tcsh (the current default shell) executes the contents of .login, but bash doesn’t. If you are using one shell that uses a particular login or profile file, and you want to change to another which uses the same file, you might struggle, or get stuck in an infinite loop. Which is probably worse.

Just having /usr/bin/bash -login in your .login file will then cause bash to run, and execute the contents of your .profile: without the -login it won’t execute the contents of said file. But what about when you exit the bash shell, using Ctrl-D, or exit, or logout, or whatever?

If you put a logout after the /usr/bin/bash -login line will cause the original shell to logout immediately after leaving the bash shell. Which is exactly what we want.

Now, all I need to do is figure out how to get rid of the line that says : tcsh: using dumb terminal settings.

That one’s too easy. Use xterm instead of xterm-color in the Settings➞Advanced area of the Terminal Preferences:


UNIX palindromes #1

Thoughts of the Bored » Archived Thought » UNIX palindromes #1

There exist a “test” command and a “tset” command, but tset does not do the reverse of test. I consider this a lost opportunity.

Getting Finder Comments in different languages

bash: mdls filename | grep FinderComment AppleScript: tell application "Finder"     comment of file end tell python: #!/usr/bin/pythonw from appscript import * path = '/Users/NAME/your/path/here' comment = app('Finder').items[path.replace('/', ':')].comment.get()

Better Random Password

An even better method of getting a random (ascii) password: $ head /dev/urandom | strings -n 5 | sed 'N;s/$//;s/\n//g;s/\n//g' | sed 'N;s/$//;s/\n//g' | head -n 1 (All on one line, naturally). I had to do the two seds to make it work properly. It would have been nicer if this combined all of the lines, and then I could just trim as many chars as I wanted, but this was tricky. The head -n 1 discards any lines other than the first.

Random Password

I needed a nice random password, something that is fairly strong. Enter /dev/urandom and md5: $ head /dev/urandom | md5 e98afcb4f093bafa0cc5f90f150df8b7 Obviously, that’s not the password I used. Problems with this method: if you don’t write it down, or save it somewhere, you will not be able to get it back. Second, it only uses a small subset of the ascii codeset - 0123456789abcdef. I’ve tried to come up with something that converts this to ascii, but I’m still working on it. You’d also likely want to ignore 8-bit values, as these can be extended characters. If you ever needed to type this in, it’s sometimes hard to do. The big advantage is that this is a totally random method, and you won’t get the same code twice.