3.4. Shell script and command line tips¶
3.4.1. Overview¶
This page contains a random assortment of useful tips and tricks for shell scripting and command line usage. It assumes that you are comfortable with basic shell syntax and commands, such as with things contained in this Unix tutorial.
We expect this page to be broadly useful across all shell
usage. Many of the features presented are available in all shells
(such as using seq
, awk
, grep
, etc.). Some features are
purely for tcsh
shell—that just tends to be the shell we prefer
for our own scripting (mainly for readability). But in such cases,
there might be equivalent features in bash
or zsh
to seek out
utilize, and learning about them here can be useful. Some sections
describe both general shell functionality and tcsh-specific items.
If you have further suggestions or examples, please let us know and we can add to the stash.
Some useful (outside) references for further shell and script learning might be:
The numbers in this section are not purposeful, and they might change over time. In general, we try to accummulate features downwards in the listing: that is, some commands get used together, but we try to assume that if you read the page from top to bottom (as you surely will, right?) then topics should be sensical.
Here we go—le deluge...
3.4.2. Know your shell type¶
To find out what kind of shell you have, type:
echo $0
in a terminal and it should tell you something like tcsh
, csh
,
bash
, zsh
, etc. If there is a minus sign at the start of the
output, that just means it is your login shell.
For most practical purposes here, tcsh
and csh
are treated as
the same.
If you want to change your shell to be, say, tcsh
, then you can
just type:
tcsh
... in the terminal. That should switch you over to that format for making/using variables, etc. When you are done, you can type:
exit
... and leave that shell.
3.4.3. Make and run a shell script¶
To make a tcsh
script, open a text file and make the first line
look like one of the following:
#!/bin/tcsh
#!/usr/bin/env tcsh
... which is called a shebang. There should be no spaces at the start.
After this line, you can put whatever commands you want for your
devious purposes. For most considerations, spacing and indentation
does not matter within the script (of course, we will mention some
exceptions below; for example, when setting a variable one needs
spaces around the assignment operator =
). However, it is a good
idea to indent your code consistently, to help yourself or other
people see where loops and conditionals end.
To run or execute the script, you can type:
tcsh SCRIPTNAME
Note that SCRIPTNAME
does not need to end with a “.tcsh”
extension, but it might help you stay organized and recognize your
script. You can add a -e
option when you run the script to have
it exit the first time a command in it fails:
tcsh -e SCRIPTNAME
You can run a tcsh script in this way from any shell variety (tcsh, zsh, bash, etc.).
Some further, convenient features:
Scripts typically dump text in the terminal. You might want to keep a copy or log of that for future reference, debugging or question-asking. You can do this as follows for a script called “SCRIPTNAME” and a logfile called “LOGNAME” (each of which can include path information, etc.):
... from a
bash
shell, run the script as:tcsh SCRIPTNAME 2>&1 | tee LOGNAME
... from a
tcsh
shell, run the script as:tcsh SCRIPTNAME |& tee LOGNAME
You can also use this feature within a script, to send the terminal output of a particular program to a text file.
NB: I can’t stress enough how useful this feature is! You should always, always, always use it. You’re welcome.
If you want to keep track of the amount of time something runs, you can start the command line with the Unix program
time
. Consider either of the following:time tcsh SCRIPTNAME time tcsh SCRIPTNAME |& tee LOGNAME
etc.
3.4.4. Check if a variable exists (tcsh)¶
To find out if a particular variable has been defined, use ?
as
$?NAME
or ${?NAME}
. The shell will return 1
if it has,
and 0
if it hasn’t. For example, I suspect this will return
0
on most systems:
echo $?CALL_ME_ISHMAEL
This can be useful to check if variable name is free or not. It can also be useful in scripting in an if condition: if a variable has been defined, then use that value; otherwise, define your own value. For example:
if ( $?CALL_ME_ISHMAEL ) then
echo "++ CALL_ME_ISHMAEL is already set to: $CALL_ME_ISHMAEL"
else
set CALL_ME_ISHMAEL = "Moby_01"
echo "+* CALL_ME_ISHMAEL was undefined; it is now: $CALL_ME_ISHMAEL"
endif
3.4.5. Store array in a variable (tcsh)¶
Shell variables can hold multiple values, acting as arrays. Consider:
set arr = ( alpha beta gamma delta epsilon zeta eta theta iota )
To find out the number of elements, use #
as follows:
echo "The number of elements in the array is: ${#arr} (= $#arr)"
You can select the whole array by providing its name:
echo $arr
You can select individual elements, which are indexed starting with
1
, up to the number of elements:
echo $arr[1]
echo ${arr[3]}
You can select “slices” (or intervals), by placing boundaries in
square brackets and separating with a dash -
; note both
boundaries are included in the interval:
echo ${arr[3-6]} # out: gamma delta epsilon zeta
You can leave off the start or stop of the boundary when specifying a slice, and the default first or last element of the array, respectively, will be used:
echo ${arr[-6]} # out: alpha beta gamma delta epsilon zeta
echo ${arr[6-]} # out: zeta eta theta iota
echo ${arr[-]} # out: alpha beta gamma delta epsilon zeta eta theta iota
3.4.6. Extract parts of filenames and/or paths (tcsh)¶
There are common conventions on many operating systems:
Directory paths are separated by the
/
character (at least in Linux and Mac). For example, someone’s home directory might be:/home/alincoln
.Filenames use
.
as a character to two components: the “base name” (or “handle”), which tells about the specific file’s identity; and the “extension”, which tells about its general type (so you/your computer knows what program to use to open it). The extension is placed at the end of the filename, and it is separated from the base by “.”. For example, in the file “assignment_v1.pdf”, the extension is “pdf” and the base is “assignment_v1”. Note that in the file “assignment.v1.pdf”, we can still recognize “pdf” as the extension and “assignment.v1”. Using “.” in the basename is fine: we treat the rightmost “.”-separated part as the extension, and the rest is a basename.
The tcsh
has useful modifiers to recognize these
features. These are applied as $var:MOD
or ${var:MOD}
,
where MOD
can be:
e
: get file extensionr
: get path+basename (the “root”), or non-extension parth
: get directory of path (the “head”)t
: get filename at the end of a path (the “tail”)
So, e
and r
complement each other, as do h
and t
.
And they can be applied multiple times, as well, as
$var:MOD1:MOD2
, ${var:MOD1:MOD2:MOD3}
, etc. Consider the
following examples:
set fff = /home/gtokeefe/Documents/painting.v1.pdf
echo $fff:e
echo $fff:r
echo $fff:h
echo $fff:t
echo $fff:h:t
echo $fff:t:r
echo $fff:e:e
echo $fff:r:r
... will output:
pdf
/home/gtokeefe/Documents/painting.v1
/home/gtokeefe/Documents
painting.v1.pdf
Documents
painting.v1
# an empty string is returned in this case
/home/gtokeefe/Documents/painting
Note that the last one (echo $fff:r:r
) might not really be what
we want, since the v1
is not really an extension to be
removed. The shell can’t read our minds (yet!), so always check the
outputs as you go.
3.4.7. Finding paths of programs (shell/tcsh)¶
Get the full path for a program in your $PATH:
which PROGRAM
You could store this in a variable with:
set var = `which PROGRAM`
This could be useful if you want to get the directory location of a
program. Say you want the file containing your AFNI binaries: first
by finding the afni
program, and then selecting the directory
containing it:
set loc_afni = `which afni`
set loc_abin = ${loc_afni:h}
echo ${loc_afni} # ex out: /home/mgandhi/abin/afni
echo ${loc_abin} # ex out: /home/mgandhi/abin
3.4.8. Make uniformly spaced numbers (shell/tcsh)¶
Generate uniformly-spaced numbers with the seq
command. This
program requires either 1, 2 or 3 arguments after.
Here are examples of each, where we name each argument by how it will be interpreted, and describe its output (though there are no commas in the actual output, just a list of numbers):
seq STOP # out: 1, 2, 3, ..., STOP
seq START STOP # out: START, START+1, START+2, ..., STOP
seq START STEP STOP # out: START, START+1*STEP, START+2*STEP, ..., STOP
Note that the default START
is 1, and STOP
is included in
the interval (unlike, say, typical Python syntax of boundaries).
The STEP
can be negative.
Examples:
seq 5 # out: 1 2 3 4 5
seq -3 3 # out: -3 -2 -1 0 1 2 3
seq 4 2 11 # out: 4 6 8 10
seq 11 -3 -2 # out: 11 8 5 2 -1
Outputs can also be stored as an array in a shell variable:
set var = `seq 4 2 11`
It can be useful to make a counter or iterator in a loop:
foreach ii ( `seq 10` )
echo "++ The counter is: ${ii}"
end
This can also combine usefully with arrays and using #
to get
the number of elements in it. Consider:
set aaa = ( omega psi chi phi upsilon tau )
foreach ii ( `seq ${#aaa}` )
echo "++ The [$ii]th value is: ${aaa[$ii]}"
end
3.4.9. String-formatting with printf
(shell/tcsh)¶
The string formatting syntax is quite to that of C programs in
print statements (of which Python borrows most for its own
str.format() method). You print a string, printf "...."
, and
for each value you want to insert into a string, you a percent
symbol and then a descriptor of the type:
%d
: integer-valued numbers%f
: floating point numbers%g
: scientific notation (1.23e+15
,4.56e-12
, etc.)%G
: scientific notation (1.23E+15
,4.56E-12
, etc.)%s
: strings
After listing your string with spaces created for values, you specify the values to be inserted in the same order. So, consider the following:
printf "%d %f %s" 10 100.1 banana # out: 10 100.100000 banana
You can control lots of features for each entry. We demonstrate some of these for the “float” type, but relevant features apply to all other types (the vertical line characters have no special meaning; they are just there to show the boundaries around the space created for the inserted value):
printf "|%12f|" 15.1
printf "|%-12f|" 15.1
printf "|%-12.3f|" 15.1
printf "|%12.5d|" 15
Note that printf
does not put a newline character \n
at
the end of a line (echo
does), so you would have to do that
yourself:
printf " %.5d %10.6f %-10s\n" 3 -21 banana
... which outputs:
00003 -21.000000 banana
Consider the following example of generating zeropadded numbers, for a filename:
foreach ii ( `seq 10` )
set jj = `printf "%.3d" ${ii}`
set fname = name_${jj}.txt
printf "++ The [%3d ]th filename is: %s\n" ${ii} ${fname}
end
3.4.10. Simple math operations (tcsh)¶
You can do simple math operations like adding, subtracting,
multiplying and dividing integers with the @
functionality. Consider:
@ aa = 10 + 5
@ bb = 10 - 5
@ cc = 10 * 5
@ dd = 10 / 5
@ ee = 10 / 3
... and echoing the outputs produces, respectively:
15
5
50
2
3
Note how these are only integer operations—note what happens in
the case of $ee
(no remainder). This can also be useful for
incrementing in place:
set vv = 1
@ vv+= 1
echo $vv
... which outputs 2
. One can also use -=
, *=
and
/=
.
This is useful, for example counting things in a loop:
set count = 0
set letters = ( a b c d e d A s a w e v s d c e w Q a )
foreach ll ( ${letters} )
if ( "${ll}" == "a" ) then
@ count+= 1
endif
end
echo "++ I found ${count} instances of 'a' in this set."
But for more complicated expressions or those involving decimals
(floating point numbers), we need something different—see the bc
operation, below.
3.4.11. More general math operations, via bc
(shell)¶
Go, Eagles!
This program allows you to write an expression with some pretty
general functionality, and have it evaluated as a calculator (that
is what the “c” in bc
stands for) would. The general syntax we
will use this is: echo "MATH EXPRESSION" | bc
. By default, the
expressions will be considered to be integer-based, but we can
specify a “scale” for the number of decimals to output. Consider
the following:
echo "10 + 15" | bc
echo "10 / 15" | bc
echo "10. / 15" | bc
echo "scale = 5; 10. / 15" | bc
... which outputs:
25
0
0
.66666
Note how even using decimal points did nothing to change output type
(like they might in some programming languages), without the
scale
being set. Note also how the scale truncates the output,
not rounding it: we would expect the last value to be .66667
,
typically.
You can save the output directly by using the fun backticks:
set output1 = `echo "10. / 15" | bc`
set output2 = `echo "scale = 5; 10. / 15" | bc`
etc.
There are lots of operators that can be included in the expression. You can also use parentheses to control order of operations, following usual math rules. You can use variables inside the expression. Some examples:
set mm = 18
echo "5 % 3" | bc # calc remainder
echo "${mm} % 3" | bc # calc remainder
echo "2^5" | bc # calc power
echo "scale = 4 ; (3.14^5) + 2" | bc # calc power
echo "scale = 3 ; sqrt( 35 )" | bc # calc sq root
You can also have comparative expressions, checking for equality
==
, inequality !=
, greater than >
, less than or equal
<=
, etc. (Though note that checking for strict inequality of
floating point numbers is not advised!) Boolean operators can be
used, as well: or ||
, and &&
, not !
. If a logical
expression evaluates to True, the output is 1; False outputs to 0.
Consider these examples:
set val = 100
echo "50 < ${val}" | bc
echo "50 < ${val} && ${val} < 200" | bc
echo "10^2 == ${val}" | bc
echo "! $val % 7 || ! $val % 3" | bc
... outputs:
1 # 50 is less than 100
1 # 50 is less than 100 AND 100 is less than 200
1 # 10-squared is equal to 100
0 # it is not true that: either 7 or 3 is a factor of 100
See the help man bc
for more information.
3.4.12. Display blocks of text with cat << EOF ... EOF
¶
You can echo
or printf
text line by line, which is often good
enough. But what if you have a block of text? You could just have
several echo
commands:
echo "# Program author: A. Lovelace"
echo "# Program version: G"
echo "# Program date: Aug 1, 1843"
echo ""
echo ""
echo "# Comment on line 1 ..."
echo ""
This quickly becomes unwieldy. A better way to go is to use the
following syntax of the cat
program (no silly, feline puns allowed
here, unless the are about fatcats):
cat << EOF
# Program author: A. Lovelace
# Program version: G
# Program date: Aug 1, 1843
# Comment on line 1 ...
EOF
Three things to note:
Spacing and empty line within the blocks are preserved, both at the start of lines and within lines.
The
EOF
is just a commonly used syntax, and you could use another string there. However, don’t use something that might occur at the start of a line in that block of text.This is a case where spacing does matter. The closing string
EOF
must occur at the start of the line. It cannot be indented. Otherwise, the shell interpreter won’t find the closing of the block.
You can include variables that have been defined outside the block in the block, just by referring to them as usual:
set N = 25
cat << EOF
echo "The value of N is: ${N}"
EOF
You can redirect the block into a text file, as well. This might be useful if you are creating a script. This is done as follows:
set N = 25
cat << EOF >> SOME_FILE
echo "The value of N is: ${N}"
EOF
This puts the three lines of text that appear in the block into
SOME_FILE
; this was done in “append” mode, so the text file
just gets longer if text were there previously. Changing >>
SOME_FILE
to > SOME_FILE
puts the operation in
“overwrite” mode, instead.
You can even define (or set) new variables inside the block of text. This might occur if you are generating a script file in this way, for example. However, you will have to “escape” the usage of any of these variables within the text block. Consider the following:
set N = 25
cat << EOF > SOME_OTHER_FILE
echo "The value of N is: ${N}"
set Nsq = N * N
The value of Nsq is: \${Nsq}
EOF
After this, SOME_OTHER_FILE
looks like the following:
echo "The value of N is: 25"
set Nsq = N * N
The value of Nsq is: ${Nsq}
If we hadn’t put a backslash \
before the usage (not definition)
of $Nsq
, then we would have gotten an error. Note that we only
did this with the variable set inside the block ($Nsq
), not the
one set outside it ($N
), because it was already evaluated before
getting to the block.
A final note: if you are going to dump in a shebang, you do not need to escape the exclamation point in it. Thus, the following would be correct:
set N = 25
cat << EOF > SOME_LAST_FILE.tcsh
#!/bin/tcsh
echo "The value of N is: ${N}"
set Nsq = N * N
The value of Nsq is: \${Nsq}
EOF
However, if you were using echo
to dump a shebang into a file, you
would need to escape the exclamation point:
echo "#!/bin/tcsh" > BAD_SCRIPT.tcsh
echo "#\!/bin/tcsh" > GOOD_SCRIPT.tcsh
You can verify this in each case.
3.4.13. Replace chars in strings (e.g., rename files)¶
Systematic renaming of files can be useful, such as replacing one set of characters with another. There can be some gotchas with this (e.g., a shell “list” of file spaces might get interpreted as a longer list of partial filenames, each original item split at the space). We examine a couple. But typically I start programming these kinds of things by just displaying the old and new names, rather than replacing each, to be sure I have things correct. I can always add a copy or move afterwards, once I have the names I want in each variable.
There are a couple replacement options: within tcsh
, you can use
the :gas/AAA/BBB/
syntax to replace “AAA” within a variable with
“BBB”. For example:
set iname = "This is some exAAAmple for chAAAraaacter replAcement"
set oname1 = "${iname:gas/AAA/BBB/}"
echo "++ OLD: ${iname}"
echo "++ NEW: ${oname1}"
This produces:
++ OLD: This is some exAAAmple for chAAAraaacter replAcement
++ NEW: This is some exBBBmple for chBBBraaacter replAcement
Note the case sensitivity in replacement of an entire string.
To remove a string, one just replaces with an empty string:
set iname = "This is some exAAAmple for chAAAraaacter replAcement"
set oname2 = "${iname:gas/AAA//}"
echo "++ OLD: ${iname}"
echo "++ NEW: ${oname2}"
... producing:
++ OLD: This is some exAAAmple for chAAAraaacter replAcement
++ NEW: This is some exmple for chraaacter replAcement
There is also the “translate” program tr
on most Linux or Mac
systems. You can use this in different ways, but piping the display
of a string through it is a fairly convenient ones for scripts. The
following matches ${oname1}
above:
set oname3 = `echo "${iname}" | tr 'AAA' 'BBB'`
One could replace spaces using either, for example with an underscore:
set oname4 = "${iname:gas/ /_/}"
set oname5 = `echo "${iname}" | tr ' ' '_'`
echo "${oname4}"
echo "${oname5}"
... equivalently producing:
This_is_some_exAAAmple_for_chAAAraaacter_replAcement
This_is_some_exAAAmple_for_chAAAraaacter_replAcement
We could loop over a set of strings and replace each one in any of the
above manners (below, we use tr
, but the :gas
route works
equivalently). One difficulty occurs if the filenames contains
spaces. Consider a directory that contains 3 screenshots, named
Screenshot 1.png
, Screenshot 2.png
and Screenshot 3.png
.
If we make a variable to store their array of names, then loop over
that array to make new names with the annoying space replaced, such as
here:
set all_files = ( Screen*png )
foreach iname ( ${all_files} )
set oname = `echo "${iname}" | tr ' ' '_'`
echo "${iname} -> ${oname}"
end
... we don’t get 3 lines output (one for each filename, which we might expect), but instead get:
Screenshot -> Screenshot
1.png -> 1.png
Screenshot -> Screenshot
2.png -> 2.png
Screenshot -> Screenshot
3.png -> 3.png
That is, the spaces were used to split each filename into two strings
before we even got there (that is, the quotes around each name are not
passed along to keep the element as a single entity), and so we
couldn’t do what we wanted. Enclosing the ${all_files}
variable
in quotes in the for-loop line won’t work: that will lead to the array
of separate items being treated like a single, giant string. However,
we can ask the shell to put a quote around each word, so that those
are not separated within the loop by putting :q
as a modifier:
set all_files = ( Screen*png )
foreach iname ( ${all_files:q} )
set oname = `echo "${iname}" | tr ' ' '_'`
echo "${iname} -> ${oname}"
end
... producing the desired:
Screenshot 1.png -> Screenshot_1.png
Screenshot 2.png -> Screenshot_2.png
Screenshot 3.png -> Screenshot_3.png
Another way around this is to do the following: don’t make a separate array that will get interpreted again. Directly loop over the filenames:
foreach iname ( Screen*png )
set oname = `echo "${iname}" | tr ' ' '_'`
echo "${iname} -> ${oname}"
end
That makes the same 3 renamings as immediately above. Finally, one could iterate over the number of elements in the array:
set all_files = ( Screen*png )
foreach ii ( `seq 1 1 ${#all_files}` )
set iname = "${all_files[$ii]"
set oname = `echo "${iname}" | tr ' ' '_'`
echo "${iname} -> ${oname}"
end