8 shell tricks DHH doesn't want you to know...

(Okay, sorry for the clickbait. There's no truth to that title aside from the fact that this post is about shell commands. Some of these are built in, others are programs you'll have to install.)

I'm one of the PLM engineering team's resident linux nerds. I practically live in the terminal. Every day I so much as touch the mouse is a failure. The point I'm trying to make is that I'm very comfortable using a terminal.

Since we all use a terminal, I like sharing some of the things I use in it with my teammates. What follows is a list of helpful basic shell commands, tricks, and apps that make my day easier. This is not a yardstick to measure your mastery over arcane shell lore (sorry, Bryan), but stuff that's useful pretty frequently.

Reverse search

This is the shell trick I find myself sharing most often with new team mates. Remember that tricky shell command you typed three months ago? You vaguely remember the name of it but can't figure out what the whole thing was or where you read about it? Or worse still, you do remember but you're too lazy to type it out. Reverse search to the rescue!

Open a terminal and hit ctrl-r. Now start typing something you've done previously. Let's go with "rails". Stop when you see a "rails c" or a "rails s" or something. You've found the first item in your shell's history that matches that string. Hit ctrl-r again and you'll go a step back to the previous command that matches. Hitting enter will run that command.

Went too far back in your history? Try ctrl-s. That's forward searching. (If that didn't work, your terminal might be doing something else with ctrl-s. Mine does. stty stop undef in zsh or stty -ixon 2>/dev/null in bash will fix it.)

Loops

Wouldn't it be great if your shell could loop like a real programming language? Guess what, it can! Isn't that great?

Here's a while loop:

while true ; do date ; done

Not the most useful thing in the world I admit. You'll have to ctrl-c that one.

There are also for loops.

for file in *.jpg ; do echo $file ; done

You can run commands to generate lists to iterate over

for number in $(seq 10) ; do echo $num ; done

(Sidenote. The seq command generates sequences of numbers)

(Another side note. The $(cmd) syntax executes cmd and uses its output inside the outer command. Backticks do this too, but without a left and right backtick you can't nest them so the $() syntax is prefered.)

And you can use semicolons to chain more commands together

for number in $(seq 20) ; do echo -- ; echo $num ; done

I mention shell loops because they came up when a teammate asked for help figuring out how to hammer a page on our server over and over. curl patientslikeme.com (which in my head canon is "see url" since it prints the contents of a url on your terminal) will print the contents of patientslikeme.com in your terminal. Stick this in a for n in $(seq 1000) and you might actually be able to make a dent on our New Relic charts.

Ag

This is just like ack, but quite a bit faster. Don't know ack? Ack is similar to grep, but with an emphasis on making pretty output for developers search coding. Don't know grep? Maybe I need to write more of these blog posts.

Ag can be found at https://github.com/ggreer/the_silver_searcher

Moving files on git

I don't know about you, but I spend a lot of time reading git history. Code usually tells me what something does, but it doesn't tell me why it does that particular thing. Seeing what's changed over the lifespan of a project helps figure out the why, at least for me.

When you rename a file using the mv command, as far as git can tell that file is deleted and a new file has sprung to life. Git doesn't necessarily know that file_b was once file_a and should use its history. But you can tell git about this by replacing mv with git mv.

Readline

Wtf is readline? "The GNU Readline library provides a set of functions for use by applications that allow users to edit command lines as they are typed in." In plain english, this is a set of edits and keybindings that let you move around text in a command line. Several popular editors use or have plugins for them as well. Most text inputs on Mac OS X also recognize these keybindings, which is one of the few features that gives me Mac envy.

Here are some of the basic ones. There are plenty more, but they can be pretty overwhelming. They're designed to keep your fingers on the home keys.

  • ctrl-p previous line
  • ctrl-n next line

These two serve the same purpose as hitting the up and down arrows to scroll through old commands, but without leaving the home key.

  • ctrl-a home
  • ctrl-e end

Again, buttons that you hit pretty often, but bound to the home key. Whenever I type out 'mv file1 file2' and suddenly remember that I should have prepended git, "ctrl-a git " is all I need to do.

  • alt-f forward word
  • alt-b backwards word
  • alt-backspace delete previous word

Here's something new. These navigate between words on a longer command. Maybe you accidentally wrote out 'rake db:imgrate', leaving the cursor after the e. alt-b takes you back to the previous word break, in this case the colon, so you can get to where you need to be to make an edit quicker.

Likewise, alt-f moves you forward by a word. I'll spare you the contrived example.

alt-backspace does the exact same motion as alt-b, but also deletes the word in the process.

(edit: Jacinda points out that these don't work out of the box in iTerm2. She said this fixes: https://coderwall.com/p/h6yfda/use-and-to-jump-forwards-backwards-words-in-iterm-2-on-os-x)

  • alt-.

This is my personal favorite. It tabs through the last word from your previous lines. Try it out: echo a, echo b, echo c.

Now hit alt and it period a few times. c. b. a.

Pretty sweet, no?

Okay, try it out when you're dealing with a file. Maybe git status says there's a merge conflict on app/views/user/_user.html.haml

You diff the file. git diff app/views/user/_user.html.haml Now you remember why you wrote that conflict. Time to edit it.

You could edit the file git diff app/views/user/_user.html.haml. But you're not an animal, you know better.

ctrl-p ctrl-a alt-f alt-f alt-backspace alt-backspace "vim" enter. You've backed up to the previous command, gone home, advanced two words, deleted two words, and opened the file in your favorite editor. That's a huge improvement, but you can still do better.

"vim " alt-. enter. Now you've chosen the command you want to run and recycled the argument to your last command. Because you're so civilized, you even thought ahead to your next command, "git add" alt-. enter. Elegant. Tidy. Majestic.

Time, script, and watch

I admit these aren't things I use every day. They're part of toolbox that are super handy when I need them, but I don't always need them. However, I decided to include them since I think they'll amuse rubyists. These commands all take other commands as arguments, kind of like a do block.

watch runs a command over and over on an interval defaulting to two seconds. Want to know when you're 10gb file is finished copying? watch ls -lh /path/to/file will show you ls's output on the file until you ctrl-c it.

Time reports how long a command took to run. I use this on my db migrations. Migrations that aren't instantaneous usually send me searching for coffee. time rake db:migrate lets me know if that migration I didn't babysit took 30 seconds or 30 minutes. Not a bad habit to get into.

Script is even more situational than these two. Running it starts a new shell session. All the commands you type and the output they produce are saved to a file. You'll have to exit or ctrl-d the session to end the recording. I like this when running through a process that I intend to document.

Aliases and functions

Aliases allow you to define an alternative name for a program. You can use them in all sorts of ways.

I shorten commands for the sake of my carpal tunnels on frequently used apps.

alias v=vim

Or to save arguments that I don't want to type out each time.

alias myip='curl http://icanhazip.com'

Or to set environment vars that I want enable as defaults.

alias nv='TERM=xterm-256color nvim'

Here's a new one I came up with this week. If you're skimming through here becasue you already know how aliases work, this is the one part of this section you should pay attention to.

Ever get really surprising errors when you try to run a rake command with arguments? You pore over the code and question your sanity and competence. But the problem was never in the code. It was in your shell.

zsh uses brackets in globbing. Not helpful when you want to use the same brackets to pass arguments to a rake task. You could quote the task and its args or escape out the brackets with a backslash. Or you could tell zsh not to glob rake tasks and never run into this problem again:

alias rake='noglob rake'

Aliases make life in the shell a whole lot more tolerable. But they're limited. When you call the rake example above, you're literally swapping out the term "rake" for "noglob rake". What if you want to do something more interesting than string substitution?

Bash and zsh also let you define functions. Functions take arguments. Here's a simple one.

Let's write a shortcut for find. Find is a crazy powerful command, but the syntax for common use cases is a little obtuse. Most of the time I use find I'm trying to locate a file in the current path, matching part of its name. I do that with something like this:

find ./ -iname "*something*"

I'd like to leave off the current path and the -iname. An alias can do that.

alias fn='find ./ -iname

But you still have to type out "something". This is where the alias gets stuck. You can't grab what comes after the alias and insert it somewhere in the middle. We're going to need a function to handle arguments.

function fn() { find ./ -iname "*$1*" }

That $1 represents the first argument passed to the function. Now I can do fn user and see all the user models, controllers, and views in my app.

Version control your dotfiles, preferably on github

This last one isn't a command or an app. It's a friendly suggestion. I was a happy desktop user for years before I started putting my settings in version control. When I finally did start doing so, I started taking my command line work more seriously. It meant that I didn't have to start from scratch each time I inherited a new machine. I could tailor my setup just so, and those specifications would persist.

Furthermore, once I started putting my dotfiles on github instead of in a private CVS (yes, I'm old), I started treating them less like a junk drawer and more like code I'd share with my team. Big time quality improvement over the pile of one liners of dubious origin I'd started with.


Anyway, I hope this was helpful to someone.