#!/usr/bin/env bash # BashBlog, a simple blog system written in a single bash script # Author: Carles Fenollosa , 2011-2012 ######################################################################################### # # README # ######################################################################################### # # This is a very basic blog system # # Basically it asks the user to create a text file, then converts it into a .html file # and then rebuilds the index.html and feed.rss. # # Comments are not supported. # # This script is standalone, it doesn't require any other file to run # # Files that this script generates: # - main.css (inherited from my web page) and blog.css (blog-specific stylesheet) # - one .html for each post # - index.html (regenerated each run) # - feed.rss (regenerated each run) # - all_posts.html (regenerated each run) # - it also generates temporal files, which are removed afterwards # # It generates valid html and rss files, so keep care to use valid xhtml when editing a post # # There are many loops which iterate on '*.html' so make sure that the only html files # on this folder are the blog entries and index.html and all_posts.html. Drafts must go # into drafts/ and any other *.html file should be moved out of the way # # TODO instead of using a file for $content, use a variable to avoid disk writes # TODO enclose all variables with quotes (thanks Durad) # TODO use a tag for dates, and keep them in sync with the html files (thanks Durad) ######################################################################################### # # LICENSE # ######################################################################################### # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . ######################################################################################### # # CHANGELOG # ######################################################################################### # # 1.4.1 Some code refactoring # 1.4 Using twitter for comments, improved 'rebuild' command # 1.3 'edit' command # 1.2.2 Feedburner support # 1.2.1 Fixed the timestamps bug # 1.2 'list' command # 1.1 Draft and preview support # 1.0 Read http://is.gd/Bkdoru ######################################################################################### # # CODE # ######################################################################################### # # As usual with bash scripts, scroll all the way to the bottom for the main routine # All other functions are declared above main. # Global variables # It is recommended to perform a 'rebuild' after changing any of this in the code global_variables() { # If you want to fork the project please contact me first, I wouldn't mind opening a git # or some shared code base and collaborate with other people. global_software_name="BashBlog" global_software_version="1.4.1" # Blog title global_title="Waiting for the jobs to finish" # The typical subtitle for each blog global_description="Thoughts on science and tips for researchers who use computers" # The public base URL for this blog global_url="http://mmb.pcb.ub.es/~carlesfe/blog" # Your name global_author="Carles Fenollosa" # You can use twitter or facebook or anything for global_author_url global_author_url="http://mmb.pcb.ub.es/~carlesfe" # Your email global_email="carles.fenollosa@bsc.es" # CC by-nc-nd is a good starting point, you can change this to "©" for Copyright global_license="CC by-nc-nd" # If you have a Google Analytics ID, put it here. # If left empty (i.e. "") Analytics will be disabled global_analytics="UA-752819-13" # Leave this empty (i.e. "") if you don't want to use feedburner, # or change it to your own URL global_feedburner="http://feeds.feedburner.com/WaitingForTheJobsToFinish" # Leave these empty if you don't want to use twitter for comments global_twitter="true" global_twitter_username="cfenollosa" } # Prints the required google analytics code google_analytics() { if [ "$global_analytics" == "" ]; then return; fi echo "" } # Edit an existing, published .html file while keeping its original timestamp # Please note that this function does not automatically republish anything, as # it is usually called from 'main'. # # 'edit' is kind of an advanced function, as it leaves to the user the responsability # of editing an html file # # $1 the file to edit edit() { timestamp="`date -r $1 +'%Y%m%d%k%M'`" $EDITOR $1 touch -t $timestamp $1 } # Adds the code needed by the twitter button # # $1 the post URL twitter() { echo "

Comments?  " echo "Tweet " echo "

" } # Adds all the bells and whistles to format the html page # Every blog post is marked with a and # which is parsed afterwards in the other functions. There is also a marker # to determine just the beginning of the text body of the post # # $1 a file with the body of the content # $2 the output file # $3 "yes" if we want to generate the index.html, # "no" to insert new blog posts # $4 title for the html header # $5 original blog timestamp create_html_page() { content=$1 filename=$2 index=$3 title=$4 timestamp=$5 # Create the actual blog post # html, head cat .header.html > $filename echo "$title" >> $filename google_analytics >> $filename echo "" >> $filename # body divs echo '
' >> $filename echo '
' >> $filename # blog title echo '
' >> $filename cat .title.html >> $filename echo '
' >> $filename # title, header, headerholder echo '
' >> $filename file_url="`sed 's/.rebuilt//g' <<< $filename`" # Get the correct URL when rebuilding # one blog entry if [ "$index" == "no" ]; then echo '' >> $filename # marks the beginning of the whole post echo '

' >> $filename echo $title >> $filename echo '

' >> $filename if [ "$timestamp" == "" ]; then echo '
'`date +"%B %d, %Y"`' — ' >> $filename else echo '
'`date +"%B %d, %Y" --date="$timestamp"` ' — ' >> $filename fi echo "$global_author
" >> $filename echo '' >> $filename # This marks the text body, after the title, date... fi cat $content >> $filename # Actual content if [ "$index" == "no" ]; then echo '' >> $filename if [ "$global_twitter" == "true" ]; then twitter "$global_url/$file_url" >> $filename fi echo '' >> $filename # absolute end of the post fi echo '
' >> $filename # content # page footer cat .footer.html >> $filename # close divs echo '
' >> $filename # divbody and divbodyholder echo '' >> $filename } # Parse the plain text file into an html file parse_file() { # Read for the title and check that the filename is ok title="" while read line; do if [ "$title" == "" ]; then title=$line filename="`echo $title | tr [:upper:] [:lower:]`" filename="`echo $filename | sed 's/\ /-/g'`" filename="`echo $filename | tr -dc '[:alnum:]-'`" # html likes alphanumeric filename="$filename.html" content=$filename.tmp # Check for duplicate file names while [ -f "$filename" ]; do suffix=$RANDOM filename="`echo $filename | sed 's/\.html/'$suffix'\.html/g'`" done else echo $line >> $content fi done < "$TMPFILE" # Create the actual html page create_html_page $content $filename no "$title" rm $content } # Manages the creation of the text file and the parsing to html file # also the drafts write_entry() { if [ "$1" != "" ]; then TMPFILE="$1" if [ ! -f "$TMPFILE" ]; then echo "The file doesn't exist" delete_includes exit fi else TMPFILE=.entry-$RANDOM.html echo "Title on this line" >> $TMPFILE echo "" >> $TMPFILE echo "

The rest of the text file is an html blog post. The process" >> $TMPFILE echo "will continue as soon as you exit your editor

" >> $TMPFILE fi chmod 600 $TMPFILE post_status="E" while [ "$post_status" != "p" ] && [ "$post_status" != "P" ]; do $EDITOR $TMPFILE parse_file "$TMPFILE" # this command sets $filename as the html processed file chmod 600 $filename echo -n "Preview? (Y/n) " read p if [ "$p" != "n" ] && [ "$p" != "N" ]; then chmod 644 $filename echo "Open $global_url/$filename in your browser" fi echo -n "[P]ost this entry, [E]dit again, [D]raft for later? (p/E/d) " read post_status if [ "$post_status" == "d" ] || [ "$post_status" == "D" ]; then mkdir -p drafts/ chmod 700 drafts/ title="`head -n 1 $TMPFILE`" title="`echo $title | tr [:upper:] [:lower:]`" title="`echo $title | sed 's/\ /-/g'`" title="`echo $title | tr -dc '[:alnum:]-'`" draft="drafts/$title.html" while [ -f "$draft" ]; do draft="drafts/$title-$RANDOM.html"; done mv "$TMPFILE" "$draft" chmod 600 "$draft" rm "$filename" delete_includes echo "Saved your draft as '$draft'" exit fi if [ "$post_status" == "e" ] || [ "$post_status" == "E" ]; then rm $filename # Delete the html file as it will be generated again fi done rm $TMPFILE chmod 644 $filename echo "Posted $filename" } # Create an index page with all the posts all_posts() { echo -n "Creating an index page with all the posts " contentfile="all_posts.html.$RANDOM" while [ -f "$contentfile" ]; do contentfile="all_posts.html.$RANDOM" done echo "

All posts

" >> $contentfile echo "" >> $contentfile echo '' >> $contentfile create_html_page $contentfile all_posts.html.tmp yes "$global_title — All posts" mv all_posts.html.tmp all_posts.html chmod 644 all_posts.html rm $contentfile } # Generate the index.html with the content of the latest 10 posts rebuild_index() { echo -n "Rebuilding the index " newindexfile="index.html.$RANDOM" contentfile="$newindexfile.content" while [ -f "$newindexfile" ]; do newindexfile="index.html.$RANDOM" contentfile="$newindexfile.content" done # Create the content file, maximum 10 entries n=0 for i in `ls -t *.html`; do # sort by date, newest first if [ "$i" == "index.html" ] || [ "$i" == "all_posts.html" ]; then continue; fi if [ $n -ge 10 ]; then break; fi awk '//, //' $i >> $contentfile echo -n "." n=$(( $n + 1 )) done if [ "$global_feedburner" == "" ]; then echo '' >> $contentfile else echo '' >> $contentfile fi echo "" create_html_page $contentfile $newindexfile yes "$global_title" rm $contentfile mv $newindexfile index.html chmod 644 index.html } # Displays a list of the posts list_posts() { lines="" n=1 for i in `ls -t *.html`; do if [ "$i" == "index.html" ] || [ "$i" == "all_posts.html" ]; then continue; fi line="$n # `awk '/

/, /<\/a><\/h3>/{if (!/

/ && !/<\/a><\/h3>/) print}' $i` # `date -r $i +%B\ %d\,\ %Y`" lines="${lines}""$line""\n" # Weird stuff needed for the newlines n=$(( $n + 1 )) done echo -e $lines | column -t -s "#" } # Generate the feed file make_rss() { echo -n "Making RSS " rssfile="feed.rss.$RANDOM" while [ -f "$rssfile" ]; do rssfile="feed.rss.$RANDOM"; done echo '' >> $rssfile echo '' >> $rssfile echo ''$global_title''$global_url'' >> $rssfile echo ''$global_description'en' >> $rssfile echo ''`date -R`'' >> $rssfile echo ''`date -R`'' >> $rssfile echo '' >> $rssfile n=0 for i in `ls -t *.html`; do if [ "$i" == "index.html" ] || [ "$i" == "all_posts.html" ]; then continue; fi if [ $n -ge 10 ]; then break; fi # max 10 items echo -n "." echo '' >> $rssfile echo "`awk '/<h3><a class="ablack" href=".+">/, /<\/a><\/h3>/{if (!/<h3><a class="ablack" href=".+">/ && !/<\/a><\/h3>/) print}' $i`" >> $rssfile echo '> $rssfile echo "`awk '//, //{if (!// && !//) print}' $i`" >> $rssfile echo "]]>$global_url/$i" >> $rssfile echo "$global_url/$i" >> $rssfile echo "$global_author" >> $rssfile echo ''`date -r $i -R`'' >> $rssfile n=$(( $n + 1 )) done echo '' >> $rssfile echo "" mv $rssfile feed.rss chmod 644 feed.rss } # generate headers, footers, etc create_includes() { echo '

'$global_title'

' > .title.html echo '
'$global_description'
' >> .title.html echo '' > .header.html echo '' >> .header.html echo '' >> .header.html echo '' >> .header.html echo '' >> .header.html if [ "$global_feedburner" == "" ]; then echo '' >> .header.html else echo '' >> .header.html fi echo '' >> .footer.html } # Delete the temporarily generated include files delete_includes() { rm .title.html .footer.html .header.html } # Create the css file from scratch create_css() { # To avoid overwriting manual changes. However it is recommended that # this function is modified if the user changes the blog.css file if [ ! -f "blog.css" ]; then # blog.css directives will be loaded after main.css and thus will prevail echo ' #title { font-size: x-large; } a.ablack { color: black !important; } li { margin-bottom: 8px; } ul, ol { margin-left: 24px; margin-right: 24px; } #all_posts { margin-top: 24px; text-align: center; } .subtitle { font-size: small; margin: 12px 0px; } .content p { margin-left: 24px; margin-right: 24px; } h1 { margin-bottom: 12px !important; } #description { font-size: large; margin-bottom: 12px; } h3 { margin-top: 42px; margin-bottom: 8px; } h4 { margin-left: 24px; margin-right: 24px; } #twitter { line-height: 20px; vertical-align: top; text-align: right; font-style: italic; color: #333; margin-top: 24px; font-size: 14px; } ' > blog.css fi # This is the CSS file from my main page. Any other person would need it to run the blog # so it's attached here for convenience. if [ `whoami` == "carlesfe" ] && [ ! -f main.css ]; then ln -s ../style.css main.css # XXX This is clearly machine-dependent, beware! elif [ ! -f main.css ]; then echo ' body { font-family: Georgia, "Times New Roman", Times, serif; margin: 0; padding: 0; background-color: #F3F3F3; } #divbodyholder { padding: 5px; background-color: #DDD; width: 874px; margin: 24px auto; } #divbody { width: 776px; border: solid 1px #ccc; background-color: #fff; padding: 0px 48px 24px 48px; top: 0; } .headerholder { background-color: #f9f9f9; border-top: solid 1px #ccc; border-left: solid 1px #ccc; border-right: solid 1px #ccc; } .header { width: 800px; margin: 0px auto; padding-top: 24px; padding-bottom: 8px; } .content { margin-bottom: 45px; } .nomargin { margin: 0; } .description { margin-top: 10px; border-top: solid 1px #666; padding: 10px 0; } h3 { font-size: 20pt; width: 100%; font-weight: bold; margin-top: 32px; margin-bottom: 0; } .clear { clear: both; } #footer { padding-top: 10px; border-top: solid 1px #666; color: #333333; text-align: center; font-size: small; font-family: Courier New, Courier, monospace; } a { text-decoration: none; color: #003366 !important; } a:visited { text-decoration: none; color: #336699 !important; } blockquote { background-color: #f9f9f9; border-left: solid 4px #e9e9e9; margin-left: 12px; padding: 12px 12px 12px 24px; } blockquote img { margin: 12px 0px; } blockquote iframe { margin: 12px 0px; } ' > main.css fi } # Regenerates all the single post entries, keeping the post content but modifying # the title, html structure, etc rebuild_all_entries() { echo -n "Rebuilding all entries " for i in *.html; do # no need to sort if [ "$i" == "index.html" ] || [ "$i" == "all_posts.html" ]; then continue; fi contentfile=".tmp.$RANDOM" while [ -f "$contentfile" ]; do contentfile=".tmp.$RANDOM"; done echo -n "." # Get the title and entry, and rebuild the html structure from scratch (divs, title, description...) title="`awk '/

/, /<\/a><\/h3>/{if (!/

/ && !/<\/a><\/h3>/) print}' $i`" awk '//, //{if (!// && !//) print}' $i >> $contentfile # Original post timestamp timestamp="`date -r $i`" create_html_page $contentfile $i.rebuilt no "$title" "$timestamp" # keep the original timestamp! timestamp="`date -r $i +'%Y%m%d%k%M'`" mv $i.rebuilt $i chmod 644 $i touch -t $timestamp $i rm $contentfile done echo "" } # Displays the help function usage() { echo "$global_software_name v$global_software_version" echo "Usage: $0 command [filename]" echo "" echo "Commands:" echo " post [filename] insert a new blog post, or the FILENAME of a draft to continue editing it" echo " edit [filename] edit an already published .html file. Never edit manually a published .html file," echo " always use this function as it keeps the original timestamp " echo " and rebuilds whatever indices are needed" echo " rebuild regenerates all the pages and posts, preserving the content of the entries" echo " reset deletes blog-generated files. Use with a lot of caution and back up first!" echo " list list all entries. Useful for debug" echo "" echo "For more information please open $0 in a code editor and read the header and comments" } # Delete all generated content, leaving only this script reset() { echo "Are you sure you want to delete all blog entries? Please write \"Yes, I am!\" " read line if [ "$line" == "Yes, I am!" ]; then rm *.html *.css *.rss echo "Deleted all posts, stylesheets and feeds." else echo "Phew! You dodged a bullet there. Nothing was modified." fi } # Main function # Encapsuled on its own function for readability purposes # # $1 command to run # $2 file name of a draft to continue editing (optional) do_main() { global_variables # Check for validity of argument if [ "$1" != "reset" ] && [ "$1" != "post" ] && [ "$1" != "rebuild" ] && [ "$1" != "list" ] && [ "$1" != "edit" ]; then usage; exit; fi if [ "$1" == "list" ]; then list_posts exit fi # Test for existing html files ls *.html &> /dev/null if [ $? -ne 0 ] && [ "$1" == "rebuild" ]; then echo "Can't find any html files, nothing to rebuild" exit fi # We're going to back up just in case tar cfz .backup.tar.gz *.html chmod 600 .backup.tar.gz if [ "$1" == "reset" ]; then reset exit fi create_includes create_css if [ "$1" == "post" ]; then write_entry "$2"; fi if [ "$1" == "rebuild" ]; then rebuild_all_entries; fi if [ "$1" == "edit" ]; then edit "$2"; fi rebuild_index all_posts make_rss delete_includes } # # MAIN # Do not change anything here. If you want to modify the code, edit do_main() # do_main $*