Ten tips for wonderful bash productivity

February 5th 2015 Nicola Paolucci in Bash, Tips

I am always tweaking and tricking my bash environment. I hit the same issues again and again and I always have to look up the solution, time after time. This happens until I get annoyed enough to sit down - okay, generally I am already sitting down but you get the point - and create a custom function, put it in my .bashrc and deploy it to any machine I log on to.

In the hope my struggle for ultimate terminal efficiency is of help to other fellow command liners have a look at some tips and functions I use frequently. It would be cool if this would turn into a two-way dialog and you also suggest your own bash shortcuts at @durdn, @atlassiandev or in the comments below!

Without further ado, here's what I've got today.

1. Add a line at the top of a file

I have to look up this every time. Here's now to add a line at the top of a text file using sed:

sed -i '1s/^/line to insert\n/' path/to/file/you/want/to/change.txt

2. Append text to a configuration file

Easy and well known, here's how to append (>>) several lines to a file. It uses the "here document" syntax which allows you to embed a document into your source by specifying which word delimits the end of the file. Most commonly used word is EOF (aka "End Of File"):

cat >> path/to/file/to/append-to.txt << "EOF"
export PATH=$HOME/jdk1.8.0_31/bin:$PATH
export JAVA_HOME=$HOME/jdk1.8.0_31/
EOF

Everything in between the first EOF and the last gets appended to the file.

3. Recursive global search and replace

If you use Eclipse, IntelliJ or any other IDE you probably have powerful refactoring capabilities at your fingertips. But I bet sometimes you work on a tech stack that does not come with so advanced capabilities.

How do I do a global search and replace on a directory tree on the command line again? Please, don't make me use Perl, what about find and sed? Thank you Stack Overflow:

# OSX version
find . -type f -name '*.txt' -exec sed -i '' s/this/that/g {} +

After a few times I have added a custom function to my .bashrc like the following:

function sr {
    find . -type f -exec sed -i '' s/$1/$2/g {} +
}

And you use it like this:

sr wrong_word correct_word

4. Open a scratch file in vim and Dropbox

I used to loved the scratch facility of Emacs and I missed the ability to quickly create a temp file to type persistent notes on my vim setup. So I came up with these two quick bash functions which use openssl to generate a random alphabetic string as filename:

function sc {
  gvim ~/Dropbox/$(openssl rand -base64 10 | tr -dc 'a-zA-Z').txt
}

function scratch {
  gvim ~/Dropbox/$(openssl rand -base64 10 | tr -dc 'a-zA-Z').txt
}

By typing sc or scratch on my terminal a new gvim or macvim windows pops up with random temporary file stored in my Dropbox folder ready for my "insightful" notes (aka random trash).

5. Download a file following redirects, with https, security concerns

Output a page to stdout (updated!) following redirects and ignoring security exceptions:

curl -Lks <some-url>

Download a file following redirects and ignoring security exceptions:

curl -OLks <some-url/to/a/file.tar.gz>

I know, I know... there's no need to note those flag down! It's enough to read the very easy and short curl documentation! (humor engine engaged at full power).

6. Bashmarks for the win

If you don't have bashmarks in your .bashrc yet, what are you waiting for? They are awesome. They allow you to save and jump back to directories you use often. I have a minimal setup for them in my configuration like the following, but the link above has a more robust solution you can load up in your .bashrc:

# USAGE:
# s bookmarkname - saves the curr dir as bookmarkname
# g bookmarkname - jumps to the that bookmark
# g b[TAB] - tab completion is available
# l - list all bookmarks

# save current directory to bookmarks
touch ~/.sdirs
function s {
  cat ~/.sdirs | grep -v "export DIR_$1=" > ~/.sdirs1
  mv ~/.sdirs1 ~/.sdirs
  echo "export DIR_$1=$PWD" >> ~/.sdirs
}

# jump to bookmark
function g {
  source ~/.sdirs
  cd $(eval $(echo echo $(echo \$DIR_$1)))
}

# list bookmarks with dirnam
function l {
  source ~/.sdirs
  env | grep "^DIR_" | cut -c5- | grep "^.*="
}
# list bookmarks without dirname
function _l {
  source ~/.sdirs
  env | grep "^DIR_" | cut -c5- | grep "^.*=" | cut -f1 -d "="
}

# completion command for g
function _gcomp {
    local curw
    COMPREPLY=()
    curw=${COMP_WORDS[COMP_CWORD]}
    COMPREPLY=($(compgen -W '`_l`' -- $curw))
    return 0
}

# bind completion command for g to _gcomp
complete -F _gcomp g

7. Extract a column from a tabular output (my most used awk trick)

I use this daily. Really. I get some output and I only want the second column of it, or the third or whichever and typing out the full command is wordy:

#Sample output of git status -s command:

$ git status -s

M .bashrc
?? .vim/bundle/extempore/

# Remove status code from git status and just get the file names
$ git status -s | awk '{print $2}'

.bashrc
.vim/bundle/extempore/

Why not create a simple function that you can use anytime?

function col {
  awk -v col=$1 '{print $col}'
}

This makes slicing columns so easy, for example you want to remove the first column? Easy:

$ git status -s | col 2

.bashrc
.vim/bundle/extempore/

8. Skip first x words in line

I am a fan of xargs, I think it's almost as good as sliced bread. But sometimes you need to massage the list you get back from it, maybe skipping a few values? For example when you want to remove stale docker images and the first line is a heading you don't need:

function skip {
    n=$(($1 + 1))
    cut -d' ' -f$n-
}

Here's how to use it:

  • The full tabulated command docker images gives back:
$ docker images

REPOSITORY                   TAG         IMAGE ID            CREATED             VIRTUAL SIZE
<none>                       <none>      65a9e3ef7171        3 weeks ago         1.592 GB
<none>                       <none>      7c01ca6c30f2        3 weeks ago         11.1 MB
<none>                       <none>      9518620e6a0e        3 weeks ago         7.426 MB
<none>                       <none>      430707ee7fe8        3 weeks ago         7.426 MB
boot2docker/boot2docker      latest      1dbd7ebffe31        3 weeks ago         1.592 GB
spaceghost/tinycore-x86_64   5.4         f47686df00df        7 weeks ago         11.1 MB
durdn/bithub                 latest      df1e39df8dbf        8 weeks ago         100.9 MB
<none>                       <none>      c5e6cf38d985        8 weeks ago         100.9 MB
nginx                        latest      e426f6ef897e        12 weeks ago        100.2 MB
zoobab/tinycore-x64          latest      8cdd417ec611        8 months ago        7.426 MB
scratch                      latest      511136ea3c5a        20 months ago       0 B
  • With the previous newt col function I can get all the image ids like this:
$ docker images | col 3

IMAGE
65a9e3ef7171
7c01ca6c30f2
9518620e6a0e
430707ee7fe8
1dbd7ebffe31
f47686df00df
df1e39df8dbf
c5e6cf38d985
e426f6ef897e
8cdd417ec611
511136ea3c5a
  • Now we are close I can almost remove them all with:
docker images | col 3 | xargs

IMAGE 65a9e3ef7171 7c01ca6c30f2 9518620e6a0e 430707ee7fe8 1dbd7ebffe31 f47686df00df df1e39df8dbf c5e6cf38d985 e426f6ef897e 8cdd417ec611 511136ea3c5a
  • But there is that spurious "IMAGE" which I'd like to, well, skip:
docker images | col 3 | xargs | skip 1

65a9e3ef7171 7c01ca6c30f2 9518620e6a0e 430707ee7fe8 1dbd7ebffe31 f47686df00df df1e39df8dbf c5e6cf38d985 e426f6ef897e 8cdd417ec611 511136ea3c5a
  • Now I can remove all of them by feeding the result to docker rmi (docker remove image command):
docker rmi $(docker images | col 3 | xargs | skip 1)

9. Create your very own command package

in bash it's incredibly easy to create your own command suite, names paced however you please. Have a look at some things I put in mine:


function dur {
  case $1 in
  clone|cl)
    git clone git@bitbucket.org:nicolapaolucci/$2.git
    ;;
  move|mv)
    git remote add bitbucket git@bitbucket.org:nicolapaolucci/$(basename $(pwd)).git
    git push --all bitbucket
    ;;
  trackall|tr)
    #track all remote branches of a project
    for remote in $(git branch -r | grep -v master ); do git checkout --track $remote ; done
    ;;
  key|k)
    #track all remote branches of a project
    ssh $2 'mkdir -p .ssh && cat >> .ssh/authorized_keys' < ~/.ssh/id_rsa.pub
    ;;
  fun|f)
    #list all custom bash functions defined
    typeset -F | col 3 | grep -v _ | xargs | fold -sw 60
    ;;
  def|d)
    #show definition of function $1
    typeset -f $2
    ;;
  help|h|*)
    echo "[dur]dn shell automation tools"
    echo "commands available:"
    echo " [cl]one, [mv|move]"
    echo " [f]fun lists all bash functions defined in .bashrc"
    echo " [def] <fun> lists definition of function defined in .bashrc"
    echo " [k]ey <host> copies ssh key to target host"
    echo " [tr]ackall], [h]elp"
    ;;
  esac
}

With the above I can copy my ssh key to any site just by typing dur key user@somehost.

Conclusions

Peruse the custom functions in my .bashrc or come up with your own. Do you have other neat tricks or short functions that help you in your daily terminal hackings? Let me know in the comments below or at @durdn on twitter. I'm always on the look for new ideas.