I have been fortunate enough to spend time using Bash to work with PHP in a Linux environment in the past. At one point in my life, I was tasked with creating/configuring headless VMs to run our WebDriver test suites on Amazon's EC2. I will admit that before I started this task I did know a few commands at the terminal. I had a few tricks up my sleeve, you might say. You might also say that those tricks were essentially "ls" and "nano" -- and you'd be right. Of course, not all of my work used Bash, but if you've had to build up a Linux server from some vanilla distro to suit your needs, then you probably understand how a chunk of time is spent at the terminal.
Fast forward a few years, and my pants size has gone up, my hair count has gone down, BUT, conversely, I'm now OK with Bash. When my colleagues need help with the command line, I'm the guy for the job.
So, what distinguishes me from my colleagues? Is it that I'm a developer wizard of sorts? (Contrary to popular belief, I did not graduate from the developer equivalent of Hogwarts. In fact, I've never been enrolled at a wizard school, and frankly do not believe such a place even exists.) But I digress -- what distinguishes me from my colleagues is that I can stumble my way through the CLI with some Bash tips and tricks that I'll share with you now!
Very Basic Customization
Open up your terminal, and using your favorite editor (I prefer emacs) edit the following file:
emacs ~/.bashrc
When you fire up your Bash terminal, this file (particular to your user) is loaded/sourced. If we want to define some customizations, here is a great place to do so. Whatever you have in this file will be loaded for whenever you open your terminal.
At the end of this file type in something like:
echo "Wow! You sure are looking great today!"
Save it, then open a new terminal, and you should notice that your terminal is pretty nice after all! But, if you're like me, you're probably worried that your terminal talks like that to just anyone. Well, here's how you might dry up that rainy face:
echo "Wow! You sure are looking great $(whoami)!"
Pretty cool, huh? Your terminal knows your name, and it's telling you personally using command substitution that you are as cute as a button!
- Instead of saving and opening up a new terminal. typing source ~/.bashrc
will load your .bashrc file
Creating Files for Our Customizations
Create a new directory under our home directory like so:
mkdir ~/.bash
This is where our customization file will live.
Let's create this file
echo '#!/bin/bash' > ~/.bash/bash_customizations.sh
and move the greeting code there.
To enable this file to be called when the terminal is opened, add this line to your .bashrc:
source ~/.bash/bash_customizations.sh
After saving, you should be able to open a new terminal to find the same warm greeting.
Aliases
Suppose you do a particular variety of "ls" often and you are tired of typing it out. Well, try an alias out for size:
alias losm='ls -lHs --ignore=".*"'
Now you can type "losm" at the command line and the command aliased to it will be executed.
The coolest thing I do is use emacs. The second coolest thing I do is tell people that I use emacs. Here's how you will open emacs inside of your terminal:
emacs -nw /path/to/some/file
Or you can use an alias and simply type "emacs". Place the following in your .bashrc or execute in your terminal.
alias emacs="emacs -nw"
Now when I execute this code
emacs reasonsWhyJasonIsGreat.txt
the actual command being executed is, because of the alias,
emacs -nw reasonsWhyJasonIsGreat.txt
Pretty great, right? Please note, that on my box, the file "reasonsWhyJasonIsGreat.txt" takes a while to load. :)
Click here for a list of aliases I find useful.
Binds
Pretty fun so far, right? Well, somethings aren't as fun -- and one of those things are traversing directories.
Traversing directories, widely viewed as the antithesis to the "cowabunga", is only redeemed by the fact that you will rarely fall victim to shark attacks while typing "cd ../". Seriously though, unlike what we're used to in Windows, selecting files and directories are case sensitive. Let's fix that by adding the following code
bind 'set completion-ignore-case on'
Now, let's talk about all the times you need to type in "cd" this and "cd" that. There's a back button in Windows, why not here? I do not know. But I do know how I emulate the back and forward buttons:
function __move-up-directory {
pushd . >> /dev/null
cd ../
}
function __move-down-directory {
popd >> /dev/null
}
Now, if you type "__move-up-directory" in your terminal, you will move up one level. But typing is hard, and typing is for nerds, and those function names are terrible. If only we could map it to some keybinding.
bind '"\e>"':"\"\C-u__move-up-directory\C-m\""
What we're done here is bound the key meta+< (or alt+shift+,) to the sequence
Ctrl+a # moves cursor to the beginning of the line
__move-up-directory # prints the function name
Ctrl+m # executes whatever is on the command line
Let's do the same thing for moving down a directory
bind '"\e<"':"\"\C-u__move-down-directory\C-m\""
Click here for a list of binds I find useful.
Functions
Here is an easy way to make a function
function foo {
echo "1st arg: ${1}, 2nd arg: ${2}, 3rd arg: ${3} ...
}
As you can see, the ${n} param indicates the n-th parameter given, from 1 on.
Suppose you want to copy certain files matching a certain Perl compatible regular expression from one directory to another. This might come in handy if working on a server with log files where every log file is prepended with a timestamp.
Here's an example of the contents of an archive:
-rw-rw-r-- 1 warhammer 0 Oct 19 22:49 2012479347300_dontcare_839488.txt
-rw-rw-r-- 1 warhammer 0 Oct 19 22:49 20124793473_dontcare_839488.txt
-rw-rw-r-- 1 warhammer 0 Oct 19 22:49 20124793473_dontcare_83943.txt
-rw-rw-r-- 1 warhammer 0 Oct 19 22:49 89733390124778973_dontcare_83943.txt
-rw-rw-r-- 1 warhammer 0 Oct 19 22:49 89790124778973_dontcare_83943.txt
-rw-rw-r-- 1 warhammer 0 Oct 19 22:48 89790124778973_file_83943.txt
-rw-rw-r-- 1 warhammer 0 Oct 19 22:48 20124778973_file_83943.txt
-rw-rw-r-- 1 warhammer 0 Oct 19 22:48 20124793473_file_83943.txt
So, here's how you'd copy the files to a new dir using a function that is only concerned with what's after the timestamp:
function copy-files-matching-pattern-to {
local file_match_pattern=${1}
local destination_dir=${2}
local files=$(ls | grep -P "${file_match_pattern}")
#Make the directory if it does not exist
mkdir -p ${destination_dir}
for file in $files; do
echo "copying file $PWD/${file} to ${destination_dir}/${file}"
cp ${file} ${destination_dir}/.
done
}
This function will create a directory (unless it already exists) with the name of the 2nd parameter passed in.
If you execute this function like this given the above directory contents
copy-files-matching-pattern-to "\d+_file" newDir
You will end up with a new directory called "newDir" with the following files
-rw-rw-r-- 1 warhammer 0 Oct 19 23:22 89790124778973_file_83943.txt
-rw-rw-r-- 1 warhammer 0 Oct 19 23:22 20124793473_file_83943.txt
-rw-rw-r-- 1 warhammer 0 Oct 19 23:22 20124778973_file_83943.txt
Now, suppose you want to do the same thing as above, but you only want to copy the files over that match a file pattern and contain a string matching some other pattern. Here's how you could accomplish that:
function copy-files-matching-pattern-contains-string-pattern-to {
local file_match_pattern=${1}
local destination_dir=${2}
local string_pattern=${3}
local filepaths=$(grep -rPo "${string_pattern}" | grep -P "$file_match_pattern")
echo $filepaths
mkdir -p ${destination_dir}
for filepath in $filepaths; do
local file=$(echo ${filepath} | grep -oP ".*(?=:${string})")
echo "copying file $PWD/${file} to ${destination_dir}/${file}"
cp ${file} ${destination_dir}/.
done
}
Click here for a list of functions I find useful.
Closing Comments
I hope with this post you might be able to find your way around the terminal and impress your friends. If you have any questions, comments, please drop a message.
Alias List
alias ll='ls -AlF'
alias la='ls -A'
alias l='ls -CF'
alias lah='ls -lAh'
alias emacs='emacs -nw'
alias irb='irb --simple-prompt'
alias lah='ls -lAh'
alias rm='rm -i'
alias df='df -h'
alias du='du -sh'
alias h='history | tail'
Bind List
bind 'set completion-ignore-case on'
# alt-S loads source
bind '"\eS"':"\"source ~/.bashrc\C-m\""
# alt-j moves cursor back word
bind '"\ej"':"\"\eb\""
# alt-k moves cursor forward word
bind '"\ek"':"\"\ef\""
# alt-J cuts word behind cursor
bind '"\eJ"':"\"\C-w\""
# alt-K cuts word in front of cursor
bind '"\eK"':"\"\ed\""
# alt-< Move up the directory. same as cd ../
bind '"\e Move down the directory.
bind '"\e>"':"\"\C-u__move-down-directory\C-m\""
# alt-L same as __ls-type
bind '"\eL"':"\"\C-u__ls-type\C-m\""
bind '"\ew"':kill-region
# case sensitive file grep
bind '"\eXf"':"\"\C-a__find-in-files \C-e . rn \C-j\""
# case insensitive file grep
bind '"\eXi"':"\"\C-a__find-in-files \C-e . rni \C-j\""
# case insensitive regex file grep, Uses Perl regex. Encase this with some quotes
bind '"\eXr"':"\"\C-a__find-in-files \C-e . rniP \C-j\""
# used with the do-git function
bind '"\eG"':"\"\C-udo-git\C-m\""
Function List
function __move-up-directory {
pushd . >> /dev/null
cd ../
}
function __move-down-directory {
popd >> /dev/null
}
function __ls-type {
local _type
read -n 1 -s _type
ls_cmd='ls -glhBAF --ignore=#* --ignore=.git --color=always'
case $_type in
"h" )
;;
"t" ) # sort by modification time
ls_cmd="$ls_cmd -t"
;;
"s" ) # sort by file size
ls_cmd="$ls_cmd -S"
;;
*)
ls_cmd="ls --color=always"
;;
esac
$ls_cmd
}
####################################################
# This is great for binding to some key combination.
# This does a recursive search through all files in
# a given dir. I bind it to ctrl+x f or ctrl+x r
####################################################
function __find-in-files {
usage=$(cat <<FIND_IN_FILES_USAGE
__find-in-files [needle] [grep_arguments] [haystack_dir]
FIND_IN_FILES_USAGE
)
local needle=${1}
local haystack_dir=${2}
local grep_arguments=${3}
if [ -z ${needle} ]; then
echo "needle is not populated"
echo -e $usage
return 1;
fi
if [ -n "${4}" ]; then
echo "there is an extra parameter at position 4: '${4}'"
echo -e $usage
return 1;
fi
if [ -z ${haystack_dir} ]; then
haystack_dir="."
elif ! [ -e ${haystack_dir} ]; then
echo -e "haystack_dir: '${haystack_dir}' DNE"
echo -e $usage
return 1
fi
if [ -z ${grep_arguments} ]; then
grep_arguments=rni
fi
local command="grep -${grep_arguments} --color ${needle} ${haystack_dir}"
echo $command
$command
}