#!/usr/bin/env bash ## Written May 14, 2010 ## Taha Ahmed # This is the first bash script where I implemented the ideas outlined by # https://betterdev.blog/minimal-safe-bash-script-template/ # I don't understand the point of traps, so I skipped "-E" # https://vaneyckt.io/posts/safer_bash_scripts_with_set_euxo_pipefail/ set -euo pipefail usage() { cat < triggers a chain of special commands, see source code. In addition, the internal execution of this script is modified by the existence of certain files in the working directory: + .knitme --> use knitr::knit() instead of pgfSweave(). + .latexmkrc --> apply RC file to latexmk command. + vc --> run vc script (integrates git with LaTeX, deprecated by gitinfo2, kept for backwards support). EOF exit } # keep track of runtime of entire script starttime=$(date +%s) # in future, consider integrating this color setup with ~/.local/bin/echo-colours.sh setup_colors() { if [[ -t 2 ]] && [[ -z "${NO_COLOR-}" ]] && [[ "${TERM-}" != "dumb" ]]; then NOFORMAT='\033[0m' # text color Black='\033[0;30m' Red='\033[0;31m' Green='\033[0;32m' Yellow='\033[0;33m' Blue='\033[0;34m' Purple='\033[0;35m' Cyan='\033[0;36m' White='\033[0;37m' # bold text and color BBlack='\033[1;30m' BRed='\033[1;31m' BGreen='\033[1;32m' BYellow='\033[1;33m' BBlue='\033[1;34m' BPurple='\033[1;35m' BCyan='\033[1;36m' BWhite='\033[1;37m' # underline and color UBlack='\033[4;30m' URed='\033[4;31m' UGreen='\033[4;32m' UYellow='\033[4;33m' UBlue='\033[4;34m' UPurple='\033[4;35m' UCyan='\033[4;36m' UWhite='\033[4;37m' # text on background color On_Black='\033[40m' On_Red='\033[41m' On_Green='\033[42m' On_Yellow='\033[43m' On_Blue='\033[44m' On_Purple='\033[45m' On_Cyan='\033[46m' On_White='\033[47m' # high intensity color IBlack='\033[0;90m' IRed='\033[0;91m' IGreen='\033[0;92m' IYellow='\033[0;93m' IBlue='\033[0;94m' IPurple='\033[0;95m' ICyan='\033[0;96m' IWhite='\033[0;97m' # bold high intensity color BIBlack='\033[1;90m' BIRed='\033[1;91m' BIGreen='\033[1;92m' BIYellow='\033[1;93m' BIBlue='\033[1;94m' BIPurple='\033[1;95m' BICyan='\033[1;96m' BIWhite='\033[1;97m' # text on high intensity background color On_IBlack='\033[0;100m' On_IRed='\033[0;101m' On_IGreen='\033[0;102m' On_IYellow='\033[0;103m' On_IBlue='\033[0;104m' On_IPurple='\033[0;105m' On_ICyan='\033[0;106m' On_IWhite='\033[0;107m' else NOFORMAT='' Black='' Red='' Green='' Yellow='' Blue='' Purple='' Cyan='' White='' BBlack='' BRed='' BGreen='' BYellow='' BBlue='' BPurple='' BCyan='' BWhite='' UBlack='' URed='' UGreen='' UYellow='' UBlue='' UPurple='' UCyan='' UWhite='' On_Black='' On_Red='' On_Green='' On_Yellow='' On_Blue='' On_Purple='' On_Cyan='' On_White='' IBlack='' IRed='' IGreen='' IYellow='' IBlue='' IPurple='' ICyan='' IWhite='' BIBlack='' BIRed='' BIGreen='' BIYellow='' BIBlue='' BIPurple='' BICyan='' BIWhite='' On_IBlack='' On_IRed='' On_IGreen='' On_IYellow='' On_IBlue='' On_IPurple='' On_ICyan='' On_IWhite='' fi } msg() { echo >&2 -e "${1-}" } # examples: # die "Some message (exiting with status 1)" # die "Some message (exiting with status -->)" 0 die() { local msg=$1 local code=${2-1} # default exit status 1 msg "$msg" simpledelay.sh 2 # short delay to aid reading last message in case terminal closes on exit exit "$code" } parse_params() { while :; do case "${1-}" in -h | --help) usage ;; -v | --verbose) set -x ;; --no-color) NO_COLOR=1 ;; -?*) die "Unknown option: $1" ;; *) break ;; esac shift done args=("$@") # Check number of arguments # this script can be run with one argument is process mode or with zero arguments in post-processing mode [[ ${#args[@]} -gt 1 ]] && die "More than one argument not supported" # we use maintrack variable to keep track of whether auxiliary menu should be entered [[ ${#args[@]} -eq 1 ]] && maintrack=true return 0 } maintrack=false # I'm not really happy with this variable *name* parse_params "$@" setup_colors clear msg "-----------------------------------------------------------------------" msg "cheRTeX -- a script for processing R--Sweave/knitr--LaTeX/TikZ projects" msg "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^" msg "MMXX -- taha@chepec.se -- CHEPEC doctoral degree project" msg "-----------------------------------------------------------------------" ## If the file .latexmkrc exists in the current directory, set ltxmkrc=TRUE ltxmkrc=false msg "--- Looking for .latexmkrc" if [ -e .latexmkrc ]; then ltxmkrc=true msg "+++ LaTeXMK RC file invoked" else msg "--- This job did not request LaTeXMK RC file" fi # define some constants path_wd=${PWD} dir_wd=$(basename $path_wd) path_thesis="/media/bay/taha/chepec/thesis" temp_folder="/media/bay/taha/chepec/tmp" # define some file types Rfiletype="R" TeXfiletype="tex" tikzfiles="*.tikz" Rnwfiles="*.Rnw" # aux files aux_acn="*.acn" aux_acr="*.acr" aux_alg="*.alg" # glossaries aux_aux="*.aux" aux_bbl="*.bbl" # bibliography aux_blg="*.blg" # bibliography aux_dep="*.dep" aux_dpth="*.dpth" aux_fdb="*.fdb*" aux_fig="*.figlist" aux_fls="*.fls" aux_glg="*.glg" aux_glo="*.glo" # glossaries aux_gls="*.gls*" # glossaries and bib2gls aux_ist="*.ist" # glossaries (makeindex style file) aux_lob="*.lob" aux_lof="*.lof" aux_log="*.log" aux_lol="*.lol" # list of hyperlinks (custom, used in thesis) aux_lor="*.lor" aux_los="*.los" aux_lot="*.lot" aux_maf="*.maf" # minitoc aux_mtc="*.mtc*" # minitoc aux_out="*.out" aux_lox="*.lox" aux_make="*.makefile" aux_map="*.map" aux_run="*.run*" aux_slg="*.slg" aux_slo="*.slo" aux_sls="*.sls" aux_tikz="*-tikzDictionary" aux_tdo="*.tdo" aux_toc="*.toc" aux_xdy="*.xdy" # glossaries (xindy) # all aux files in a string auxfiles="${aux_acn} ${aux_acr} ${aux_alg} ${aux_aux} ${aux_bbl} ${aux_blg} ${aux_dep} ${aux_dpth} ${aux_fdb} ${aux_fig} ${aux_fls} ${aux_glg} ${aux_glo} ${aux_gls} ${aux_ist} ${aux_lob} ${aux_lof} ${aux_log} ${aux_lol} ${aux_lor} ${aux_los} ${aux_lot} ${aux_maf} ${aux_out} ${aux_lox} ${aux_make} ${aux_map} ${aux_mtc} ${aux_run} ${aux_slg} ${aux_slo} ${aux_sls} ${aux_tikz} ${aux_tdo} ${aux_toc} ${aux_xdy}" if $maintrack; then # Check if the argument contains a filetype # (assumes a complete filename was passed) jobfilename=${args[0]} jobfiletype=${jobfilename#*.} # File extension jobname=${jobfilename%.*} # Filename without extension part msg " Detected filename: $jobname" msg " Detected extension: $jobfiletype" # Verify that the file extension is "[Rr][Nn][Ww]" if [[ $jobfiletype == "Rnw" || $jobfiletype == "rnw" || $jobfiletype == "RNW" ]]; then # File extension is indeed "[Rr][Nn][Ww]" msg " Detected *.Rnw extension" jobfiletype="Rnw" else # File extension is NOT "[Rr][Nn][Ww]" die "This script only supports *.Rnw files" fi # Introducing a short delay to enable on-screen reading of previous echo simpledelay.sh 2 #### Special treatment for thesis if [[ $path_wd == "$path_thesis" && $jobname == "$dir_wd" ]]; then # Fetch external assets by reading any assets.external files in assets/ tree # NOTE: be careful NOT to leave empty lines in your assets.external files msg " -------------------------------" msg " Getting external assets" msg " -------------------------------" # find all files named "assets.external" in the assets/ tree assetsexternalfiles=$(find "$path_wd/assets/" -type f -name "assets.external") for externalfilepath in $assetsexternalfiles; do # https://stackoverflow.com/questions/10929453/read-a-file-line-by-line-assigning-the-value-to-a-variable while IFS='' read -r asset || [[ -n "$asset" ]]; do # $asset contains one line from the current external.assets # assetpathtarget is the directory of the current external.assets file assetpathtarget=$(dirname "$externalfilepath") # if $asset contains a space, the second element should be considered a local target folder for the copy operation # https://www.tutorialkart.com/bash-shell-scripting/bash-split-string/ # https://stackoverflow.com/a/30212526 # Using space as separator was not working if the path contains spaces # (I tried surrounding each path with "" or escaping each space with backslash, did not help) # Rather than rewriting this part, I'll change to an IFS char that's unlikely to clash with any path specification. This way, spaces in paths should not need any changes. IFS='>' # reset IFS # asset is read into an array as tokens separated by IFS read -ra asset_array <<< "$asset" # sanity check for array length if [ ${#asset_array[@]} -gt 2 ]; then die " Cannot handle more than one $IFS character per line" fi if [ ${#asset_array[@]} -gt 1 ]; then # msg "Placing this asset into subdirectory ${asset_array[1]}" # redefine assetpathtarget to include the local subdir assetpathtarget="$assetpathtarget/${asset_array[1]}" # also redefine $asset so we keep only the asset path asset="${asset_array[0]}" # create the local subdir inside assets/ Copying $asset to $assetpathtarget" # cp but don't overwrite existing files cp --preserve=timestamps --no-clobber --recursive $asset $assetpathtarget # except we want to overwrite the BibTeX library files (inside the assets/references/ directory), we'll do that by checking the target dirname and only running the destructive cp operation if its "references" assetdirnametarget=$(basename "$assetpathtarget") if [[ $assetdirnametarget == "references" ]]; then msg " Overwriting BibTeX libraries in assets/references/" cp --preserve=timestamps $asset $assetpathtarget fi done < "$externalfilepath" done # Create low-res photos on-the-fly from existing photos/ msg " -------------------------------" msg " Create low-res photos tree" msg " -------------------------------" # copy existing photos to assets/photos/.lowres/ path # to save time, rsync only if highres photo has more recent timestamp (otherwise, keep lowres photo without overwriting) # Note: rsync usually looks at file timestamp and size, and if either has changed, copies the file (simplified explanation) # in this case, I'd like rsync to only compare timestamps and disregard size # rsync can't do that. We need to use a different tool. See e.g. # https://superuser.com/questions/260092/rsync-switch-to-only-compare-timestamps # copy only the "large" photos that have file modtimes more recent than the last time this operation was run photoslastrun="$path_thesis/assets/photos/.lowres/lastrun" if [ ! -f "$photoslastrun" ]; then # if, for some reason, the lastrun file does not exist # copy over everything and then create the file # (except for the .lowres tree itself, and any assets.external files) rsync -av "$path_thesis/assets/photos/" "$path_thesis/assets/photos/.lowres/" --exclude ".lowres/" --exclude "assets.external" touch "$photoslastrun" fi # Detect new photos and copy them into .lowres tree # https://stackoverflow.com/questions/9612090/how-to-loop-through-file-names-returned-by-find # https://stackoverflow.com/questions/5241625/find-and-copy-files cd "$path_thesis/assets/photos" find . -type f -cnewer $photoslastrun ! -path "./.lowres/*" ! -name "*.external" -print -exec cp --parents "{}" .lowres \; # revert the effects of cd above. Redirect to null suppresses the output. cd - >/dev/null # Convert all non-JPG images (except for PDF, SVG, and other non-images) to JPG msg " Convert all non-JPG images to JPG" find "$path_thesis/assets/photos/.lowres/" -type f ! -name "*.pdf" ! -name "*.svg" ! -name "*.external" ! -name "lastrun" ! -name "*.jpg" -print -exec mogrify -format jpg "{}" \; # Remove the now redundant non-JPG files in .lowres/ msg " Delete the redundant non-JPG files in .lowres/ tree" find "$path_thesis/assets/photos/.lowres/" -type f ! -name "*.pdf" ! -name "*.svg" ! -name "*.external" ! -name "lastrun" ! -name "*.jpg" -print -exec rm "{}" \; # Shrink image filesize in-place to roughly 300kb in size msg " Shrink images in .lowres/ tree to <=300K" # https://stackoverflow.com/questions/6917219/imagemagick-scale-jpeg-image-with-a-maximum-file-size find "$path_thesis/assets/photos/.lowres/" -size +500k -type f -name "*.jpg" -print -exec convert "{}" -define jpeg:extent=300kb "{}" \; # update the modification and access time on the photosastrun file touch "$photoslastrun" fi ## Handle knitr or pgfSweave jobs (each requires separate treatment) ## But how should we tell the difference between them? ## There is no obvious way to tell the difference (apart from reading the *.Rnw file) ## IN ALL KNITR DIRECTORIES, CREATE A FILE NAMED: .knitme # If the file .knitme exists in the current directory, # run knitr commands, otherwise run pgfsweave commands msg "--- Looking for .knitme" if [ -e .knitme ]; then # Run knitr commands for this job msg " -----------------------" msg " This is a job for knitr" msg " -----------------------" # Knit msg " Knitting..." Rscript -e "library(knitr); library(methods); knit('$jobname.$jobfiletype')" # Introduce delay to give time to read Rscript exit status msg " -----------------------" msg " Rscript knitr completed" msg " -----------------------" simpledelay.sh 2 else # Run pgfSweave commands msg " ---------------------------" msg " This is a job for pgfSweave" msg " ---------------------------" # Tangle msg " Tangling..." R CMD Stangle $jobname.$jobfiletype # Weave msg " Weaving..." R CMD pgfsweave --graphics-only $jobname.$jobfiletype # Introduce delay to give time to read R CMD exit status msg " -------------------------" msg " R CMD pgfsweave completed" msg " -------------------------" simpledelay.sh 2 fi # Run vc script if vc exists in working directory msg " Running vc script..." if [ -f vc ]; then ./vc fi # Run pdflatex, bibtex, and company if $ltxmkrc; then msg "${On_Cyan} Calling LaTeXMK with RC file${NOFORMAT}" simpledelay.sh 2 latexmk -r .latexmkrc -pdf -bibtex $jobname else msg "${On_Cyan} Calling LaTeXMK${NOFORMAT}" simpledelay.sh 2 latexmk -pdf -bibtex $jobname fi else # Zero arguments (the case of more than one argument is handled in parse_params above) # In this case, present a menu of choices msg "This is cheRTeX POST-PROCESSING" # only one choice for now msg "<1> 'pdf-all' -- Process all .tikz files to pdf graphics" msg "<2> 'clean-up' -- Remove all auxiliary files" msg "<3> 'wipe-dir' -- Remove all non-essential files and subdirectories" msg "Any other input exits the program" read usrchoice if [[ $usrchoice == "pdf-all" || $usrchoice == "1" ]]; then msg "<1> 'pdf-all' chosen" # This for loop ONLY USED to determine number of *.tikz files in directory for tikzfiles in "$tikzfiles"; do tikzfilenumber=${#tikzfiles}; done msg "cheRTeX detected $tikzfilenumber TikZ files for processing" msg "Starting TikZ file processing..." simpledelay.sh 2 for tikzfilename in $tikzfiles; do # Call tikz2pdf msg " tikz2pdf $tikzfilename" tikz2pdf --once $tikzfilename done msg "Completed TikZ file processing" fi if [[ $usrchoice == "clean-up" || $usrchoice == "2" ]]; then msg "<2> 'clean-up' chosen" rm $auxfiles # Still, a rather crude way of cleaning up... fi if [[ $usrchoice == "wipe-dir" || $usrchoice == "3" ]]; then msg "<3> 'wipe-dir' chosen" ## Remove all but non-essential files # get a timestamp timestamp=$(date +%s) # create a unique tmp-dir name tmpdirname="${timestamp}-${dir_wd}" # make a directory in chepec/tmp with the name of the current dir mkdir $temp_folder/$tmpdirname # Copy the contents of the current directory to the tmp/$currdirname directory cp * -R $temp_folder/$tmpdirname # Empty the current directory of all contents rm * -R # note: hidden files and subdir unaffected # Return the stuff we want to keep after wipe-dir # (we are of course assuming that the following 4 file(type)s always exist) # (if in fact they do not exist, use conditional statements instead (see below) cp $temp_folder/$tmpdirname/*.Rnw . cp $temp_folder/$tmpdirname/vc . cp $temp_folder/$tmpdirname/vc.tex . cp $temp_folder/$tmpdirname/vc-git.awk . ## Return stuff that may not always exist (check first...) ## The use of conditionals is mainly to avoid annoying "file does not exist" messages... # Return *.Rproj file (removal is unnecessary and makes RStudio less useful) Rprojfiles=`ls -1 $temp_folder/$tmpdirname/*.Rproj 2>/dev/null | wc -l` if [ $Rprojfiles != 0 ]; then cp $temp_folder/$tmpdirname/*.Rproj . fi # Return *.rda files (considering peak-data files, which "cost" a lot to create) rdafiles=`ls -1 $temp_folder/$tmpdirname/*.rda 2>/dev/null | wc -l` if [ $rdafiles != 0 ]; then cp $temp_folder/$tmpdirname/*.rda . fi # Return *.Rmd files (R markdown source files) Rmdfiles=`ls -1 $temp_folder/$tmpdirname/*.Rmd 2>/dev/null | wc -l` if [ $Rmdfiles != 0 ]; then cp $temp_folder/$tmpdirname/*.Rmd . fi # Return *.css files (css files) [for sample-matrix] cssfiles=`ls -1 $temp_folder/$tmpdirname/*.css 2>/dev/null | wc -l` if [ $cssfiles != 0 ]; then cp $temp_folder/$tmpdirname/*.css . fi # Return .knitme file [empty file used to indicate knitr jobs] knitmefile=`ls -1 $temp_folder/$tmpdirname/.knitme 2>/dev/null | wc -l` if [ $knitmefile != 0 ]; then cp $temp_folder/$tmpdirname/.knitme . fi fi die "All done. Exiting..." 0 fi # Depending on whether the clock uses summer or wintertime, the date string length # will differ by one (CEST vs CET). # Just to be neat, we will take this into consideration when constructing the # "job completed" block below. cetcest=$(date +%Z) # keep track of runtime of entire script endtime=$(date +%s) runtime=$(( $endtime - $starttime )) # send push message via Gotify CLI # if runtime is longer than X minutes (suitable limit perhaps 3 min) if (( $runtime > 180 )); then gotify push --quiet --title "$dir_wd" --priority 5 "chertex.sh $@ \nCompleted in $runtime s" fi msg "${On_Cyan}-------------------------------------${NOFORMAT}" # the padding for runtime makes the formatting work # three digits for seconds is enough for just above 15 minutes printf "${On_Cyan}=== chertex.sh completed in %03d s ===${NOFORMAT}\n" $runtime 1>&2 if [[ $cetcest == "CET" ]]; then msg "${On_Cyan}=== $(date) ===${NOFORMAT}" else msg "${On_Cyan}=== $(date) ===${NOFORMAT}" fi msg "${On_Cyan}-------------------------------------${NOFORMAT}" simpledelay.sh 3 exit 0