Bash files: Difference between revisions
Created page with "== Bash and <code>.sh</code> files == '''This article is still being developed - released earlier to align with Department seminar.''' Bash files (often denoted with a post-..." |
No edit summary |
||
| Line 1: | Line 1: | ||
[[Category:Terminal and Command Line]] | |||
== Bash and <code>.sh</code> files == | == Bash and <code>.sh</code> files == | ||
Latest revision as of 02:52, 29 June 2026
Bash and .sh files
This article is still being developed - released earlier to align with Department seminar.
Bash files (often denoted with a post-script .sh to the file name) can provide a way to store multiple bash commands that can then be run with a single command:
bash my_bash_script.shSome common uses include:
- looping through multiple parameters
- creating a set of files / folders
- edit files in minor ways
bash for-loops
Terminal level automatization can occur when you are able to view your work as a set of command-line calls, and often this can entail multiple runs of similar commands - which makes them naturally benefit from using a for-loop. Below is a very simple example of a for-loop that runs a python script 10 times (it may be useful to read Python on the server). If we suppose we put the below code in a file called my_bash_script_py.sh then we could run it with bash my_bash_script_py.sh.
#!/bin/bash
for IDX in {1..10}
do
echo "Step $IDX/10"
ipython -i my_file.py $IDX
doneLet’s break down the above code:
- First, the
#!/bin/bashis a comment line with a “bang” (aka#!) that tells the computer that you will actually be using thebashlanguage when you evaluate the file. - This for-loop varies the
IDXvariable for integers between 1 to 10 (inclusive). To use theIDXvalue later we use the notation$IDXwhich can be seen in lines 4 and 5 (notice how it can be included inside quotes for theechocall and outside quotes for theipythoncall). The 5th line takes in$IDXas the first parameter for the python filemy_file.py(see Python on the server and R on the server for more details about passing arguments in from the command line).
Defining more complex bash for-loops
The above for loop is very simple and constrained about what values you are going through. The below table shows some examples of more complex calls
| code | description |
|---|---|
for VALUE in 100 200 400 ...
|
$VALUE takes the value 100, 200, and 400
|
for VARIABLE in |
$VARIABLE takes the text1, text3, text7, ...
|
$idxtakes the values 0, 2, 4, 6, 8, 10 |
| |
EXTERNAL_NUM=10
for i in $seq(1 2 $EXTERNAL_NUM) ... |
itakes odd values between 1 and$EXTERNAL_NUM(inclusive). Where we defined theEXTERNAL_NUM right before
|
For more examples of for-loops in bash see cyberciti.biz
Examples
Below we provide two examples (from Ben LeRoy and Mike Stanley respectively). These try to capture the complexities you can do with bash scripts.
Example 1: Parallelization
Warning: below shows how to parallelize inside a bash script. This can be dangerous since it sends processes to the background so it’s harder/impossible to cancel. This can be very bad as your processes can use too much RAM and crash your computer…
Script’s goal: to process 60 jobs (with parameters $BIDX and $INDEX) with some parallelization (maximum 6 jobs run at once) on a local computer.
#!/bin/bash
N=6 # maximum number of jobs run at once
for BIDX in {1..6}
do
echo "--"
echo "Number of Sims Idx: $BIDX"
echo "--"
for INDEX in {1..10}
do
Rscript 2-assess_sensitivity.R $BIDX $INDEX default default &
if [[ $(jobs -r -p | wc -l) -ge $N ]]; then
wait
fi
done
wait
done
echo "all completed"Code breakdown:
- [Line 3] We set
N=6to capture a global variable ($N) is the total number of jobs that can be used by this script. This constraint with be identified below. Since this relates to the number of jobs run at the same time, one should be aware about the number of cores your computer has and the amount of RAM you use for each job (the later we’ll cause memory overload if you’re not careful). - [Line 5:9] Our first for-loop goes across
$BIDXintegers between 1 and 6 (making some printouts as it does so). - [Line 10:11] We have an inner for-loop with
$INDEXtaking integer values between 1 and 10. - [Line 12] For each of the inner for-loops, we run an
.Rscript that takes in 4 parameters ($BIDX,$INDEX,defaultanddefault– see R_on_the_server for more details). The&at the end of the line sends the code to be run in the background. - [Line 13:15] Inside the inner for-loop and after the
Rcode is run, we have anifstatement (with the ending of the if statement denotedfi). The logic of theifstatement (inside the[[ ]]) is started with the$-sign, and runs the commandjobs -r -p(which creates a list of the jobs running in the background) and then pipes that intowc -l(which counts) the total number of these jobs running. This value is then compared (-ge $N) with N, and if greater or equal to$N, then the if statement is executed. - [Line 14] If the
ifstatement is executed then it executes the functionwaitwhich just waits until the jobs sent to the background (with the&) are finished before proceeding. - [Line 16] The
donestatement completes the inner for-loop - [Line 17] Another
waitcall makes sure that all of those jobs sent to the background for the inner for-loop are done before proceeding - [Line 18] The
donestatement completes the outer for-loop - [Line 19] Prints out the message “all complete”. Given the second
waiton line 17, this will occur only after allBIDXvalues are completely processed.
More information on parallelizing with bash can be found here: cyberciti.
Example 2: complex file creation, rewriting and changing preferences
The following code is complex so we will be breaking it into 4 parts (relative to the 4 for-loops it contains.) Please image all these steps were in a single bash file. Below is the header that was in the original file:
#!/bin/bash
#
# Script to set up a multijob directory
#
# 1. Creates one cmdfile for every 4 jobs (list of file paths to the element bash scripts)
# 2. Creates each bash script in the above file
# 3. Creates directory for each element in the above
# 4. Creates PBS script for each cmdfile
#
# INPUTS
# 1. Target directory where files and directories can be made
# 2. Template element bash script
# 3. Pre-compiled GEOS-Chem directory
#
# NOTE:
# - the sample element bash script is expected to be in the same directory
# as this script
#
# STEPS FOR USE
# 1. set directory where multijob will be executed (BASE_DIR)
# 2. set directory where compiled code lives (COMPILED_CODE_DIR)
# 3. define number of ensemble elements (NUM_ELEMENTS)
#
# Author : Mike Stanley
# Created : October 27, 2020
# Last Modified : November 24, 2020
#==============================================================================Step 1
Step’s goal: The goal of the code in this step is to (1) first define a set of global variables (some focused on the overall location of files) and then (2) create a set of files in a set of subfolders
# operational params
BASE_DIR=/glade/work/mcstanley/monte_carlo/multijob
COMPILED_CODE_DIR=/glade/u/home/mcstanley/gc_adj_runs/monte_carlo_element_const_start
NUM_ELEMENTS=20
# stems
SCRIPT_STEM=element
DIR_STEP=ens
LAUNCH_STEM=launch_file
# create and fill the cmdfile
CMDFILE_NUM=0
FILE_NUM=0
for i in $(seq 0 $( expr ${NUM_ELEMENTS} - 1 ))
do
echo $BASE_DIR/${SCRIPT_STEM}_${i}.sh >> $BASE_DIR/cmdfile_$FILE_NUM
# update indices
CMDFILE_NUM=$((CMDFILE_NUM + 1))
if [ $((CMDFILE_NUM % 4)) -eq 0 ]; then
FILE_NUM=$((FILE_NUM + 1))
fi
doneCode breakdown:
- [Line 2:4, 7:9] define global variables
- [Line 12:13] define global variables for the for-loop. The first is just an index of the for loop (@Mike - why not just use
$i?). The second will be updated every 4 files are created and helps define new folders. - [Line 14:15] starts a for-loop that will see
$igo from 0 to 19 (inclusively), given that on line 4 we defined$NUM_ELEMENTS=20. - [Line 16] The function call
echo _1_ >> _2_adds the_1_as the final text line of the file_2_(it also creates_2_if it doesn’t already exist)._1_is the pasting of the$BASE_DIR,/,$SCRIPT_STEM,_,$iand.sh(as similar thing is true for_2_. - [Line 18] (@Mike - again I’m not sure why this included). This line just updates the
$CMDFILE_NUMto1plus it’s original value - [Line 20:22] This is an
ifstatement that checks if the variable$CMDFILE_NUM’s remainder after defining by 4 (mod=%) equals (-eq)0. If so it then updates the$FILE_NUMto1plus it’s original value. Ends theifstatement withfi - [Line 24] ends the for loop from line 12
Step 2
Step’s goal: create additional bash files (associated with numbering above) and edit those bash files to have ENS_NUM also associated with the new name of the file.
# copy sample bash script and alter element number
for i in $(seq 0 $( expr ${NUM_ELEMENTS} - 1 ))
do
# create the copy for element i
cp ./sample_element.sh $BASE_DIR/${SCRIPT_STEM}_${i}.sh
# alter the ENS_NUM= line
# NOTE: there might be platform dependence with the sed command
replace_str="ENS_NUM=${i}"
sed -i.bu "s/ENS_NUM=0/${replace_str}/" $BASE_DIR/${SCRIPT_STEM}_${i}.sh
# remove the created backup
rm $BASE_DIR/${SCRIPT_STEM}_${i}.sh.bu
doneCode breakdown:
- [Line 2:3, 15] see lines 14:15 and 24 in step 1.
- [Line 5] make a copy of
./sample_element.shwith name and in the directory$BASE_DIRwith name$SCRIPT_STEM_${i}.sh(so for the first for-loop step, we’d have it be calledelement_0.sh(remember$BASE_DIRwas defined in step 1’s line 2). - [Line 9] define a new variable
replacement_string - [Line 10] use the
sedcommand to relace all strings of the formENS_NUM=0with your$replace_strin the newly created fill (from line 5). See geeksforgeeks for more details on the power ofsed. - [Line 11] Since we have to extra parameter
sed -i.buin line 10, we delete a back-up file created fromsed(@Mike - I’m not 100% sure this is true - and is it needed?)
Step 3
Step’s goal: change file privileges of script files and create and fill new directories associated with new scripts (from step 2)
# give executable privileges to the above scripts
chmod ugo+x $BASE_DIR/${SCRIPT_STEM}_*
# create the code directories for each element
for i in $(seq 0 $( expr ${NUM_ELEMENTS} - 1 ))
do
# make the directory
mkdir $BASE_DIR/ens_${i}
# make the directory by copying over contents
cp -r $COMPILED_CODE_DIR/* $BASE_DIR/ens_${i}
doneCode breakdown:
- [Line 2] change file privileges for all the new script files using
chmod. Note thatchmod ugo+xupdates the executability privileges of the file to theuser that owns it, members in the user’sgroup and thoseoutside the user’s group. See computerhope for more references. This was necessary due to default file privileges on the server and how user structure was setup. - [Line 5:6] start of a for-loop for variable
$iranging between0and$NUM_ELMENTS - 1- aka for all newly make files in Step 2. - [Line 8] make new directories assocated with each new script (@Mike: should this be
mkdir ${BASE_DIR}/${DIR_STEP}_${i}? ) - [Line 11] copy the full directory (
-r: recursively) of the$COMPILED_CODE_DIR/into our newly make folders
Step 4
Step’s goal: copy a launch script into a set of new folders, then update the copied script by changing the name of the cmdfile it is using. (@Mike: is the LAUNCH_STEP folders already created?)
# create PBS run files for each cmdfile
CMD_FILE_NMS=$BASE_DIR/cmdfile*
COUNT=0
for file in $CMD_FILE_NMS
do
# create the copy for cmdfile i
cp ./sample_launch_script $BASE_DIR/${LAUNCH_STEM}_${COUNT}
# replace the cmdfile name
replace_str="cmdfile_${COUNT}"
sed -i.bu "s/\/cmdfile/\/${replace_str}/" $BASE_DIR/${LAUNCH_STEM}_${COUNT}
# remove the created backup
rm $BASE_DIR/${LAUNCH_STEM}_${COUNT}.bu
COUNT=$((COUNT + 1))
doneCode breakdown:
- [Line 2] define a list of variable names starting with a certain structure
- [Line 4, 17] define a variable that starts at
0and add1to it each time the following loop runs - [Line 5:6, 14] a for-loop across the file names in the list defined in line 2.
- [Line 8] copy file
./sample_launch_scriptinto file into new older - [Line 10:11, 13] replace the string
/cmdfileto/cmdfile_[i]where i in a new index (in the file you just created with the copy in line 8). Line 13 removes temporary file creates with the replacement