Useful scripts

aragats

Daemon

Reaction score: 503
Messages: 1,223

(about parts of the command line disappearing)
....
I'm not sure from mind what causes this, come to think of it I suppose it could also be an effect of setting up a specific prompt...
I don't see such thing with ksh93 in urxvt.
ShelLuser , since you use Xorg, have you tried running resize when such things happen?
I noticed that sometimes terminals get screwed up by "bad outputs" of certain commands.
 

ShelLuser

Son of Beastie

Reaction score: 1,715
Messages: 3,537

Thanks for your comments, both of you :)

Out of curiosity, why is ksh your favorite? And which one of the various implementations?
I use shells/pdksh on all my servers, and the 'why' part has everything to do with SunOS / Solaris. There is no rational reasoning other than ksh being the standard on Sun Solaris and that was the very first Unix environment I learned to operate (and I loved every part of it).

Around the 90's during my internship within Olivetti computers did I learn about this 'magical' operating system called Unix which could outperform Windows on every possible geek level. Transporting files between PC's? No need for floppies nor a special parallel cable with laplink or Norton Commander. Instead even a serial cable would do and you'd be able to simply communicate between those machines with nothing other than the Unix OS itself. Even Windows 3.11 couldn't pull that off!

Learned about Linux (RedHat 'Picasso') and dove in head first.

So when I became a sysadmin at my first job and suddenly discovered that a sub-group within our company used a Sun Sparcstation running Sun Solaris ("Unix") plus Smartstream (database server) while they were having issues I knew exactly what to do. Well, I didn't but I knew man man, learned about catman and then apropos then and there. Fixed the network hiatus by switching the server from DHCP client (provided by a flakey Novell server) to a static IP and everyone was happy. So happy that I was sent onto 2 Sun Solaris training camps, paid for by said company. That was the beginning of the end ;)

I didn't switch from Linux to FreeBSD, I switched from Solaris 10/x86 to FreeBSD :)

Back then (around 2009 I think?) FreeBSD was the perfect Solaris replacement for me; UFS & ZFS, Jails vs. Zones, ipf (Solaris firewall) and a sane user land.

I exchanged ipfilter for pf last year or so, but I'm still a die hard Korn shell user ;)

Note that I don't use it for scripting or such, only interaction. Mine even runs in vi mode by default, I love it:

Code:
peter@zefiris:/home/peter $ cat .kshrc
if [ "`basename ${0#-}`" == "ksh" ]; then
   set -o posix -o braceexpand -o vi -o vi-tabcomplete;
   PS1="`whoami`@`hostname -s`:\${PWD} \$ ";
fi
Very important detail is that for me it's not only about work, I also actually enjoy working with this stuff.

I really should change my PS1 sometime, these days you can swap out your home for ~ or make it cut longer pathlines into smaller bits but I never took the time to do so and just kept using what I already had & once learned :)

Sorry for a brief ramble there, but yah ;)

ShelLuser , since you use Xorg, have you tried running resize when such things happen?
I noticed that sometimes terminals get screwed up by "bad outputs" of certain commands.
No, never bothered with resize(1) (did look it up as you can tell ;) ). But this has nothing to do with X directly though, this also happens if I'm on a regular console (I never automatically log onto the GUI).

But it's shell related. As olli@ mentioned above; this doesn't happen with sh (which I also tried) but more so with ksh. Still, it doesn't bother me, also because my vi mode allows to be move back and forth onto such a commandline with easy (escape ^; done).
 
Last edited:

olli@

Well-Known Member
Developer

Reaction score: 277
Messages: 315

I am a hobbyist with respect to IT. But this is why I really like shells/ksh93. I have not tried shell/pdksh yet. Learning some key bindings and use them for many tools is marvelous.
But vi-mode is not unique to ksh. All common shells support vi-mode line editing. bash and zsh do, and even FreeBSD's /bin/sh supports it (even though it's otherwise not very well-suited for interactive use).
 

k.jacker

Aspiring Daemon

Reaction score: 391
Messages: 678

If anyone has ever tried to format multimedia/mplayer's "ICY Info" output strings from a script, has maybe grown some grey hair.
There are a lot of examples on the web using grep, cut, awk and sed, and that's what I was using, too.
Still it had some weaknesses.

Today I stumbled over some of my own examples from earlier this year, where I was learning to use back references.

What I use now, has become so short and simple, with no weaknesses anymore, I'd like to share it.
Code:
mplayer -msglevel all=0:demuxer=4 <url to radio stream> | grep --line-buffered -oe "\([\']\)[[:print:]]*\1"
Nice, tmux statusline ready output :D
'Dynoro feat. Gigi D'Agostino - In My Mind'
'Udo Lindenberg - Cello (feat. Clueso)'
'www.antennemv.de'
'Depeche Mode - People Are People'
'Milow - Lay Your Worry Down (feat. Matt Simons)'
'The Avener - Fade Out Lines'
'Jason Mraz - Have It All'
'R.E.M. - Losing My Religion'
'Adele - Someone Like You'


What the backreference does is, it greps everything between the first and the last single quote from the string:
ICY Info: StreamTitle='Dynoro feat. Gigi D'Agostino - In My Mind';

Redirect the output to a file and add the following to ~/.tmux conf
Code:
set -g status-left '#(tail -n1 /tmp/<filename>)'
 

Sensucht94

Well-Known Member

Reaction score: 390
Messages: 388

pdksh, and all its idependent derivatives (mksh, GNU ksh, OpenBSD ksh -oksh-, NetBSD ksh, AmigaOS SKsh) are originally born as free AT&T ksh88 clones, the way Linux is Unix clone (completely written from scratch, different at its core, compatible at its surface); the ksh one can find on commercial Unices, is AT&T ksh93, a new overall reshaped and improved version of ksh88, first released in '94, opensource under Eclipse Public License as of 2005, Illumos default system interpreter, developed until 2014 when the last version was released, and presumably now discontinued.
dtksh (CDE's shell, used also in the dtlogin Xsession script) and tksh are ksh93 derivatives providing shell-level mapping for Motif and Tk widget toolkits respectively. rksh is ksh93 acting in restricted mode

Although many patches providing cross-compatibility with new ksh93 additions were submitted to the various pksh versions throughtout the years, pdksh derivatives and ksh93 are not fully compatible, while pdksh derivatives usually are with one another

Generally speaking ksh93 is more complex (have a look at sources on github), one may say bloated;
Of all the pdksh derivatives, mksh is the most featured, providing bash/zsh -like extentions, while oksh is the most stripped down, clean, fast, but also barebones

None of those shells are POSIX-compliant by default (in spite of their significantly stronger adherence to POSIX compared to bash), but can operate in POSIX mode (set -o posix), which is what they do when invoked as /bin/sh
 

Vull

Well-Known Member

Reaction score: 119
Messages: 285

I used the AIX version of ksh for years in my work environment which was dominated by AIX, SCO Openserver 5, and Red Hat 6.2, and it was my favorite shell. Here's an old script I wrote for changing the PS1 prompt, just to remind myself and other programmers which machine we had telnetted into, and to put the pathname in the prompt, which still wasn't all that common a thing in the 90s, and SCO still had just the "$ " or "# " prompts, and lacked the capability of putting the pwd present working directory pathname in the prompt. It's a bit crude and I'm almost ashamed to admit I wrote it, but it worked then and still works now. I no longer use AIX, SCO, or Red Hat, and have since modified it to work on FreeBSD and MacOS X 10.5.8, but it still has some of the old residue from those original three systems. Originally it was intended to use different sets of braces in the set {},[],<> to indicate the "flavor" of the OS, and it still has some of that in there too...

Edited March 12, 2019 - this is the latest improved version as just tested on freebsd 12.0-RELEASE, Debian 9.6, and Mac OS X 10.5.8 - this version uses id -u to detect if a user has used su -m to gain super-user status in FreeBSD, and accordingly shows "#" instead of "$" at the end of the prompt, even when the USER and HOME environment variables are neither root nor /root (assuming that the user's default SHELL Is /bin/sh).
Code:
#!/bin/sh
# wasatps1 - format PS1 shell prompts for bash, ksh, or sh shells
# 2019-03-12 t jch
#
# --- Usage:  PS1=`wasatps1`;export PS1
#
# --- Show $PWD (print working directory) if shell is capable (bash or ksh).
# --- Show hostname or user name as specified locally by SHOWHOST and SHOWUSER.
# --- Indicate if user is superuser by "$" (user) or "#" (super-user) suffix.
#
SHOWHOST=1 #--- 0=don't show hostname in shell prompt, 1=show it.
SHOWUSER=1 #--- 0=don't show username in shell prompt, 1=show it.
PROMPTLINES=1 #--- 1=1 line prompt, 2=2 line prompt
              #--- NOTE: sh doesn't interpret "\n" and so won't display 2 lines
#
#--- Only interpret bash, ksh, or sh shells. While checking the shell name, also

#--- set CANPWD=1 if shell prompt is capable of displaying the working directory

if [ "`echo $SHELL |grep -c /bash`" != "0" ];then
        THISSH=bash
        CANPWD=1
elif [ "`echo $SHELL |grep -c /ksh`" != "0" ];then
        THISSH=ksh
        CANPWD=1
elif [ "`echo $SHELL |grep -c /sh`" != "0" ];then
        THISSH=sh
        CANPWD=1
else #--- if neither bash, ksh, or sh is the shell, return $PS1 unchanged
        echo "$PS1"
        exit 0
fi
#--- Sniff for AIX, Linux, SCO Openserver, Mac OSX, and FreeBSD systems. Also
#--- set ISSUPER=1 if shell is running as super-user.
ISSUPER=0
if [ -d /usr/lpp/bos ];then
        FLAVOR=aix #--- IBM AIX operating system for RISC system architecture
        PREFIX=\< ; SUFFIX=\>
        if [ "$LOGIN" = "root" ];then ISSUPER=1 ; fi
elif [ "`which freebsd-version`" != "" ];then
        FLAVOR=freebsd
        PREFIX=\( ; SUFFIX=\)
        #if [ "$HOME" = "/root" ]||[ "$USER" = "root" ];then ISSUPER=1; fi
        if [ "`id -u`" = "0" ]; then ISSUPER=1; fi
elif [ "`ls /boot/vmlin* 2>/dev/null`" != "" ];then
        FLAVOR=linux #--- presumably Red Hat Linux 6.2 or newer ---#
        PREFIX=\[ ; SUFFIX=\]
        if [ "$HOME" = "/root" ];then ISSUPER=1 ; fi
elif [ -d /var/opt/K/SCO ];then
        FLAVOR=sco #--- presumably SCO Unix OpenServer 5 or better ---#
        PREFIX=\( ; SUFFIX=\)
        if [ "`env |grep '_=' |grep -c su`" = "1" ];then ISSUPER=1 ; fi
        if [ "$PS1" = "# " ]||[ "$LOGNAME" = "root" ];then ISSUPER=1 ; fi
elif [ -f /mach_kernel ];then
        FLAVOR=mac #--- presumably Apple Mac OS X or higher ---#
        PREFIX=\{ ; SUFFIX=\}
        if [ "$HOME" = "/var/root" ];then ISSUPER=1; fi
else #--- if none of the above systems, return $PS1 unchanged & exit normally
        echo "$PS1"
        exit 0
fi
#--- Get host name if specified, but strip off any domain name suffixes.
if [ "$SHOWHOST" != "0" ];then
        SHOWHOST=`hostname`
        SHOWHOST=`echo $SHOWHOST |awk 'BEGIN { FS = "." } { printf("%s",$1) }'`
fi
#--- format the shell prompt for the host, shell, and super-user level at hand
if [ "$SHOWUSER" != "0" ] || [ "$SHOWHOST" != "0" ];then
        PS1=$PREFIX; else PS1=
fi
if [ "$THISSH" = "bash" ];then
        if [ "$SHOWUSER" != "0" ];then PS1=$PS1"\u" ; fi
        if [ "$SHOWHOST" != "0" ];then PS1=$PS1"@\h" ; fi
else
        if [ "$SHOWUSER" != "0" ];then PS1=$PS1$LOGNAME; fi
        if [ "$SHOWHOST" != "0" ];then PS1=$PS1"@"$SHOWHOST; fi
fi
if [ "$CANPWD" != "0" ];then
        if [ "$SHOWUSER" != "0" ]||[ "$SHOWHOST" != "0" ];then
                PS1=$PS1" "
        fi
        if [ "$THISSH" = "sh" ];then
                PS1=$PS1"\w"
        else
                PS1=$PS1"\${PWD}"
        fi
fi
if [ "$SHOWUSER" != "0" ] || [ "$SHOWHOST" != "0" ];then
        PS1=$PS1$SUFFIX
fi
if [ $PROMPTLINES -eq 2 ];then PS1=$PS1"\n"; fi

if [ "$ISSUPER" != "0" ];then
        PS1=$PS1"# "
else
        PS1=$PS1"$ "
fi
echo "$PS1"
exit 0
 
Last edited:

Vull

Well-Known Member

Reaction score: 119
Messages: 285

Self-extracting installer script:

The "exex" script has 2 functions. If it's executed with command line arguments, it attempts to perform the Create() function, and if it has no command line arguments, it attempts to perform the Extract-and-Execute function. This has been tested on FreeBSD 11.2 and 12.0, and on Debian 9.5 and 9.6.

1. The "Create()" function creates a single executable file by concatenating 3 separate files, one after the other:
1. A. The first file included in the self-extracting executable is the exex script itself.
1. B. The second file included in the self-extracting executable is a separate script which will run right after the archive is extracted. In the example provided here, it will unzip the archive and start the installation of the software in the archive.
1. C. The third file included in the self-extracting executable is the archive itself.

2. The "Extract()" function is the function which extracts the archive from the concatenated file. After the extraction has been done, the 2nd file will be executed automatically. The automatic execution depends on the fact that the exex script has no 'exit' statement, and therefore, since the 2nd file comes right after the first, control will pass immediately from the last executed statement in the first file to the first executable statement in the 2nd file. For further documentation please refer to the comments in the two scripts. The first script is the exex file itself, and the 2nd script is an example of what can be done automatically after the archive has been extracted.

exex
Code:
#!/bin/sh
# 2018-12-19 w jch - modified for better posix compliance
#-------------------------------------------------------------------------------
# exex - create the SXE (mode=c), or (ex)tract & (ex)ecute the SXE (mode=x)
# ----   ------     ---  ------       --          --           ---  ------
# SXE = (S)elf-e(X)tracting-(E)xecutable script file

Create() { #--- Create the whole self-extracting archive and executable script.

  #--- get the file size of this file, which will be the first file in the sxe,

  size1=`ls -l $0 |awk 'BEGIN{FS=" "}{printf("%s",$5)}'`

  #-- and the size of the user-provided script file, which will be the 2nd file,

  size2=`ls -l $script |awk 'BEGIN{FS=" "}{printf("%s",$5)}'`
  
  #--- and the size of the user-provided archive, which will be the 3rd file

  # size3=`ls -l $archive |awk 'BEGIN{FS=" "}{printf("%s",$5)}'`

  #--- NOTE: we don't actually need to know size3, hah, so I commented it out.

  #--- Build a candidate line 2, which will be interpreted as a comment when
  #--- the combined script files run. The two scripts will execute one after the
  #--- other, like a single script. The size info line 2 comes right after the
  #--- shebang line, i.e., the line that says: "#!/bin/sh" ... The magical
  #--- importance of the inserted 2nd line is that it provides info to the
  #--- Extract() function, as to exactly where the user-provided archive file
  #--- begins, so it can be extracted from the sxe, and made into a usable
  #--- copy of the original archive. Thus, it must also contain the name of that
  #--- original user-provided archive. This Create() function will also insert
  #--- an "exit 0" at the end of the user provided script file, so the shell
  #--- won't try to execute the archive, which will be tucked in this file when
  #--- the exex script and the user provided script are executed together, as
  #--- one. The shell will interpret the two scripts together in one pass, like
  #--- they were a single script, and that's what makes it "self-executing."

  exitcommand="exit 0"    #--- The actual length of this line will be 7,
  # exitcommandlength=7   #--- 6 for the characters, plus 1 for the "\n" char.

  #--- Now begins a series of approximations of the exact offset of the archive.

  offset=$(($size1+$size2+7+4+${#archive})); # We know where the 7
  #--- in this offset sum came from, and the 4 in the same calculation is for
  #--- the length of the 3 # chars and the \n in the line 2 info comment. Then
  #--- we add the length of the NAME of the archive file, NOT the length, or
  #--- the size, of the actual archive file, because the name of the file will
  #--- be the 2nd item of info in the info comment line.
  
  #--- Now the 2nd, closer approximation: The problem is the decimal string len.
  offset2=$(($offset+${#offset})) #--- Add string length of offset.
  offslen=${#offset} #--- The approx. length of the decimal string.
  offslen2=${#offset2} #--- offslen is 1 estimate, offslen2, closer

  #- Make offslen2 the EXACT offset, after 1st seeing if the length "rolls over"
  #- (like speedometer mileage), for example, 999 is 3 digits, 1000 is 4 digits.

  if [ $offslen2 -gt $offslen ]; then $offset2=$(($offset2+1)); fi
  line2='#'$offset2'#'$archive"#" #--- Now, finally, offset2 = the exact offset

  # Oh I know, I could've just used a few # chars for padding, but it was fun:)!

  #--- Create a temporary filename for the tmp directory,

  tmpf=$HOME/grip/tmp/exexarchive.`wasat.who` #( wasat.who = pts.0, tty1, etc.,)
                                              #( something unique to this user.)

  #--- add the first line of this file (the shebang line) to the temporary file,

  line1=`awk 'NR==1{print $0}' $0` #(Select NR (Number of Record)=1,which means)
                                   #(the 1st line. (R)ecord=row, (F)ield=column)
  echo $line1 > $tmpf

  #--- followed by the 2nd info or comment line, which we need to insert, so the
  #--- Extract() function can use that info, and will know where to find it,

  echo $line2 >> $tmpf

  #--- Now, add lines 2 through the end of file, of the exex file, after line 2,

  tail -n +2 $0 >> $tmpf

  #--- and then add all the lines of the user supplied script file after that,

  cat $script >> $tmpf

  #--- and add the exit 0 command after the user supplied script, just in case,
  #--- because we wouldn't want the shell interpreter interpreting the archive.

  echo $exitcommand >> $tmpf

  #--- and add the archive file,

  cat $archive >> $tmpf

  #--- and rename it, at the same time moving to the path specified in the name,

  mv $tmpf $sxe
  chmod +x $sxe #--- and then, finally, make the whole file executable.

  echo 'Finished creating "'$sxe'"'
}

Extract() { #--- Extract the user-provided archive file, and then the 2nd part
            #--- of this file will execute, right after this function executes.

  #--- The shell interpreter's timing considerations will make the 2nd script
  #--- wait for the archive to be extracted, just like we have to wait for a
  #--- command to finish before the shell shows us the next command prompt.

  line2=`head -n 2 $0 |awk 'NR==2{print $0}'`
  offset=`echo $line2 |awk 'BEGIN{FS="#"}{printf("%s",$2)}'`
  archive=`echo $line2 |awk 'BEGIN{FS="#"}{printf("%s",$3)}'`
  if [ ! -f $archive ]; then
    tail -c +$(($offset+1)) $0 > $archive
  fi
}

Syntax() { #--- Complain about syntax errors and/or any type of errors, & exit.

  if [ "$mes" != "" ]; then
    echo
    echo "Error:  *** "$mes" ***"
  fi
  echo
  echo "Syntax:"
  echo "  exex scriptFile archiveFile nameForNewSelfExtractingExecutable"
  echo
  exit 1
}

#--- ma(in lo)gic

#--- Determine if we're running in create mode, or in extract and execute mode.

mode= #--- if filename is exex, run in create mode, otherwise, use ex & ex mode

mes= #--- error message for the Syntax() function

name=`echo $0 |awk 'BEGIN{FS="/"}{printf("%s",$NF)}'` #--- this filename

if [ "$name" = "exex" ]; then mode=c; else mode=x; fi

case "$mode" in

  ("c") #--- Create the archive

    #--- First check the command line arguments.
    exex=$0
    script=$1
    archive=$2
    sxe=$3 #--- S.X.E. is bad language for (S)elf e(X)ecuting (E)xecutable.

    if [ "$sxe" = "" ]; then Syntax; #--- If last one is empty all 3 are empty.
    elif [ ! -f $script ]; then
      mes='Script file: "'$script'" not found.'; Syntax
    elif [ ! -f $archive ]; then
      mes='Archive file: "'$archive'" not found.'; Syntax
    elif [ -f $sxe ]; then
      mes='Target file: "'$sxe'" already exists.'; Syntax 
    fi

    Create
     ;;

  ("x") #--- Extract the user-provided archive file, which is file section 3 in
        #--- this weird file, and then let the shell interpreter start
        #--- interpretting the user-provided script, which is file section 2.
        #--- We are now close to the end of file section 1. Thanks for visiting.

    Extract
     ;;
  (*) ;;
esac

#--- PROGRAMMER(S): THIS IS IMPORTANT! Do NOT add an exit command, and PLEASE,

# exit 0 #--- PLEASE, DON"T uncomment THIS line, or the user's script file
#        won't be executed! Which would thwart the whole purpose of this file.

#--- BECAUSE: we want the two scripts to run together, like a single script.
#-------------------------------------------------------------------------------
execute-install
Code:
#!/bin/sh
# execute-install - bootstrap script for self-extracting installer which s.b.
#   created by the non-root user running exex w/ this 2 step command sequence:
#
# cd $HOME
# exex grip/install/freebsd11.2.d/execute-install xfer.tar install-wasat
#
# Note: this is not an executable script, but rather, install-wasat will be.

# 2018-8-30 h 081 jch |# 2018-8-31 f 079 jch # 2018-9-16 n 088 jch
# 2018-10-15 m 094 jch |# 2018-10-20 m 094 jch |# 2018-11-3 s jch
# 2018-11-24 s jch - from grip/install/debian9.5.d -> grip/install/freebsd11.2.d

dv='11.2'
installdir='grip/install/freebsd'$dv'.d'
subnet=dashnet

oldpath=`pwd`
cd $HOME
#--- Check if system is partially installed (and still needs to have setup done)

echo "Checking if system is already (partially) installed ... "

h=/usr/local/www/apache24/data
if [ -f $h/wasat/setup-wasat.php ]; then
  DISPLAY=:0; export DISPLAY

  firefox "https://`hostname`.$subnet/wasat/setup-wasat.php" &

  cd $oldpath
  exit 0
fi
#--- Check if system appears to be completely or partly/unsuccessfully installed

if [ -d $h/wasat ]||[ -d $HOME/grip ]; then
  echo
  if [ -d $h/wasat ]; then
    echo 'Your system appears to be already installed in "'$h'/wasat/".'
  else
    echo "Your system appears to be partially installed."
    echo "There is a 'grip' directory in your home directory."
  fi
  cd $oldpath
  exit 0
fi
#--- Start unpacking the xfer.tar archive

echo -n "... Preparing to install the pre-packaged software ... "

tar -xf xfer.tar
tar -xzf xfer/grip.tar.gz
if [ ! -d $HOME/Desktop ];then mkdir $HOME/Desktop; chmod 700 $HOME/Desktop; fi
if [ ! -d $HOME/Desktop/0 ]; then mkdir $HOME/Desktop/0; fi
if [ "`ls xfer/wasat* 2>/dev/null`" != "" ]; then
  cp -p xfer/wasat* Desktop/0/
fi
echo
#--- Execute the install script

echo "... Starting the installation process ..."

$installdir/install |tee $installdir/install.log

inp=
if [ -f $h/wasat/setup-wasat.php ]; then
  echo
  while [ "$inp" != "y" ]&&[ "$inp" != "n" ];do
    echo -n "Would you like to continue with the setup now? [y/n] "; read inp;
    if [ "$inp" != "" ]; then inp=`echo $inp | tr [:upper:] [:lower:]`; fi
    case "$inp" in ("yes") inp=y;; ("no") inp=n;; ("y");; ("n");;
      (*) echo "Please answer yes or no (or y or n)."; inp="";;
    esac
  done
  if [ "$inp" = "y" ]; then
    echo "Launching the setup program now..."
    DISPLAY=:0; export DISPLAY
    firefox "https://`hostname`.$subnet/wasat/setup-wasat.php" &

    #--- message to keep terminal open in case installer was desktop-launched
    echo
    echo "Please leave this Terminal open until your Firefox session is"
    echo "closed, or your Firefox session may also be suddenly closed."
    echo
  fi
fi
cd $oldpath
exit 0
 

Vull

Well-Known Member

Reaction score: 119
Messages: 285

Shell script to display FreeBSD package dependencies recursively:

Code:
#!/bin/sh
# pkg-dependencies - recursively display package dependencies / FreeBSD 12.0R
# 2019-1-18 f jch

# Uses a /tmp dir. work file to avoid listing the same packages more than once.
# May be interrupted with CTRL-C

trap Cleanup 1 2 3 6

Cleanup() {
  #-- delete the work file(s)
  rm -rf /tmp/pkg-dependencies-work*
  exit 1
}
Recurse() {
  pkgroot=$1
  if [ "$2" = "" ];then
    lvl=0 #-- recursion level
    pid=$$ #-- pid of top level parent process
  else
    lvl=$2
    pid=$3
  fi
  workf=/tmp/pkg-dependencies-work.$pid
  if [ $lvl -eq 0 ]; then
    #-- create work file
    rm -rf $workf
    touch $workf
  elif [ ! -f $workf ]; then
    exit 1 #-- program interrupt must have occurred and deleted the work file
  fi
  #-- generate list of dependent packages using pkg info command
  list=$(pkg info -d $pkgroot)
  n=0
  for pkg in $list; do
    n=$(($n+1)) #-- sequence number in list from pkg info command
    if [ $n -eq 1 ]; then #-- this is package name arg followed by a ":" char
      #-- display the top level package name followed by ":"
      if [ $lvl -eq 0 ]; then echo 'Package dependencies for '$pkg; fi
    else
      #-- for sub-packages, display package name after a "tab line" of "." chars
      tab=
      x=$(($lvl+1)) #-- display one "." for level 0, 2 for level 1, etc.
      while [ $x -gt 0 ]; do
        tab=$tab"."
        x=$(($x-1))
      done
      #-- check work file to learn if package name has already been displayed
      grepout=$(grep $pkg $workf)
      echo $pkg >>$workf #-- add package name to work file
      if [ "$grepout" = "" ]; then #-- package has not been displayed so do it
        echo $tab $pkg #-- display tab line and package name
        $0 $pkg $(($lvl+1)) $pid #-- invoke recursion to show sub-dependencies
      fi
    fi
  done
  if [ $lvl -eq 0 ]; then
    rm -f $workf
  fi
}

Syntax() {
  echo
  if [ "$mes" != "" ]; then
    echo "Error:  *** "$mes" ***"
    echo
  fi
  echo "Syntax: pkg-dependencies packageName"
  echo
  exit 1
}

mes=
if [ "$1" = "" ];then Syntax; fi
Recurse $*
exit 0
 

olli@

Well-Known Member
Developer

Reaction score: 277
Messages: 315

While we're at it ... This is my version of a script that displays package dependencies recursively. It doesn't use any temporary files.
Code:
#!/bin/sh -
#
#   This script calls "pkg info" repeatedly in order to generate
#   recursive lists of dependecies (-d) or required-by (-r).
#

set -Cefu
ME="${0##*/}"

DEBUG=false
#DEBUG=true

if $DEBUG; then
        echo "#### $ME $*" >&2
fi

Usage()
{
        echo "Usage:  $ME {-d | -r} <package> [...]" >&2
        exit 1
}

if [ $# -lt 2 ] || [ "x$1" != x-d -a "x$1" != x-r ]; then
        Usage
fi

FLAG="$1"
shift

ARGS="$*"
LIST="%"

while :; do
        if $DEBUG; then
                echo "==== pkg info -q $FLAG $ARGS" >&2
        fi
        PKGS="$(pkg info -q $FLAG $ARGS)" || {
                echo "WARNING: Exit code from pkg: $?" >&2
        }
        if $DEBUG; then
                if [ -z "$PKGS" ]; then
                        echo "---- No output." >&2
                else
                        echo "---- Output:" >&2
                        echo "----" $PKGS >&2
                fi
        fi
        if [ -z "$PKGS" ]; then
                break
        fi
        NEW_ARGS=""
        for P in $PKGS; do
                case "$LIST" in
                        *"%${P}%"*)
                                ;;
                        *)
                                LIST="${LIST}${P}%"
                                NEW_ARGS="$NEW_ARGS $P"
                                ;;
                esac
        done
        if [ -z "$NEW_ARGS" ]; then
                break
        fi
        ARGS="$NEW_ARGS"
done

echo -n "${LIST#%}" | tr '%' '\n'
The script is called with either -d or -r, followed by the name of the package. The meaning of the two options is the same as for pkg info, see the pkg-info(8) manual page.
 

Sensucht94

Well-Known Member

Reaction score: 390
Messages: 388

Simple screencast ksh script to record audio/video on FreeBSD using ffmpeg with OSS backend, 60fps, automatic display size adjustment, progressive naming

Code:
#!/usr/bin/env mksh

today="$( date +"%Y%m%d" )"
number=0

while [[ -f $HOME/Video/record_$today$count.mkv ]]
do
    (( ++number ))
    count="$( printf -- '-%02d' "$number" )"
done
fname="$HOME/Video/record_$today$count.mkv"

ffmpeg -y \
    -thread_queue_size 1024 \
    -f oss -i /dev/dsp4 -ac 2 \
    -f x11grab -r 60 \
    -s $(xdpyinfo | grep dimensions | awk '{print $2;}') \
    -i :0.0 -c:v libx264rgb -crf 0 -preset ultrafast -c:a flac \
$fname
 

olli@

Well-Known Member
Developer

Reaction score: 277
Messages: 315

Simple screencast ksh script to record audio/video on FreeBSD using ffmpeg with OSS backend, 60fps, automatic display size adjustment, progressive naming
Just a small note: With a few simple changes, the script would run with FreeBSD's /bin/sh, so people don't have to install an additional shell.
  • Replace #!/usr/bin/env mksh with #!/bin/sh
  • In the “while” line, replace the double brackets with single brackets: [[ ... ]][ ... ]
  • Replace (( ++number )) with : $(( number += 1 )) (note the colon at the beginning!)
Alternatively, I think the script would also run unchanged with bash (except for the “shebang line” at the beginning, of course), which probably most people have installed anyway.
 

aragats

Daemon

Reaction score: 503
Messages: 1,223

In most cases (especially with multi-monitor setup) you want to record only a part of the screen.
Below I added xinput command to get the recording area.
I used left-click for the top-left and right-click for the bottom-right corners. You can use only left-clicks, but will have to uncomment sleep 1 to filter out bouncing.
Also, you'll need to get your pointer device ID (mouse=x) by running xinput --list.
For sake of simlicity I used xinput output directly as bash arrays (it should work in some other shells too), for simple sh you'll have to parse it. (I have to use bash anyway).
Code:
#!/usr/bin/env bash

mouse=6         # get this ID from "xinput --list"
audio=/dev/dsp5
today=$(date +"%Y%m%d")
fname_prefix="/tmp/screen_"
number=0

echo Select top left corner by clicking button 1
while true ; do
    #sleep 1
    eval `xinput --query-state $mouse | grep 'button\|valuator'`
    if [ "down" = "${button[1]}" ] ; then
        X0=${valuator[0]}
        Y0=${valuator[1]}
        break
    fi
done
echo Got: X0=$X0 Y0=$Y0

echo Select bottom right corner by clicking button 3
while true ; do
    #sleep 1
    eval `xinput --query-state $mouse | grep 'button\|valuator'`
    if [ "down" = "${button[3]}" ] ; then
        X1=${valuator[0]}
        Y1=${valuator[1]}
        break
    fi
done
echo Got: X1=$X1 Y1=$Y1

X=$((X1-X0))
Y=$((Y1-Y0))
if [ $X -le 0 ] || [ $Y -le 0 ] ; then
    echo You got negative dimensions: ${X}x${Y}
    exit 1
fi
echo Video size: ${X}x${Y}

while [ -f $fname_prefix$today$count.mkv ]
do
    $((number+=1))
    count=$(printf -- '-%02d' $number)
done
fname="$fname_prefix$today$count.mkv"

ffmpeg -y \
    -thread_queue_size 1024 \
    -f oss -i $audio -ac 2 \
    -f x11grab -r 60 \
    -s ${X}x${Y} \
    -i :0.0+$X0,$Y0 -c:v libx264rgb -crf 0 -preset ultrafast -c:a flac \
    $fname

echo $fname
 

Sensucht94

Well-Known Member

Reaction score: 390
Messages: 388

Just a small note: With a few simple changes, the script would run with FreeBSD's /bin/sh, so people don't have to install an additional shell.
  • Replace #!/usr/bin/env mksh with #!/bin/sh
  • In the “while” line, replace the double brackets with single brackets: [[ ... ]][ ... ]
  • Replace (( ++number )) with : $(( number += 1 )) (note the colon at the beginning!)
Alternatively, I think the script would also run unchanged with bash (except for the “shebang line” at the beginning, of course), which probably most people have installed anyway.
As I've always used ksh as interactive shell, I tend to script on it too (nver used bash or alikes), but I was definitely considering learning sh(1),...thank you for your clarifications, that's good starting point
 

Sensucht94

Well-Known Member

Reaction score: 390
Messages: 388

This is a simple flac2mp3 solution to convert songs recursively in a dir form lossless to mp3 encoding:

Code:
#!/bin/sh

for song in *.flac;
  do title=`echo "${song%.*}"`;
  echo $title;
  ffmpeg -i "$song" "${title}.mp3";
done
While this saves XA_CLIPBOARD content to a dotfile, using xclip(1)
Code:
#!/bin/sh

xclip -o | xclip -sel clip
echo $(xclip -o -sel clip) >> ~/.clipboard
gsed -n '/^\s*$/d; G; s/\n/&&/; /^\([ -~]*\n\).*\n\1/d; s/\n//; h; P' -i ~/.clipboard

notify-send "'$(xclip -o -sel clip)' saved." &
 

Maxnix

Well-Known Member

Reaction score: 197
Messages: 322

Sensucht94 , you don't need to use echo to assign the song name to the title variable. Just assign like this:
Code:
title="${song%.*}"
The shell will perform the substitution for you. :)
 

mrclksr

Active Member

Reaction score: 122
Messages: 153

Batch-encode all *.wav files in a given directory. Use each wav-file's metadata to create the ID3 tags of the corresponding mp3-file.
Code:
#!/bin/sh

preset="standard"
title_arg="--tt"
album_arg="--tl"
artist_arg="--ta"
date_arg="--ty"
genre_arg="--tg"
track_arg="--tn"
comment_arg="--tc"

usage() {
    echo "Usage: $(basename $0) [-p medium|standard|extreme|insane] dir"
    exit 1
}

while [ $# -gt 0 ]; do
    case "$1" in
    -p)
        preset=$2
        case "$2" in
        medium|standard|extreme|insane)
            preset=$2
            ;;
        *)
            usage
            ;;
        esac
        shift
        ;;
    -*)
        usage
        ;;
    *)
        break
        ;;
    esac
    shift
done

[ $# -lt 1  ] && usage

for cmd in ffprobe lame; do
    if ! which -s $cmd; then
        echo "$cmd not installed"
        exit 1
    fi
done
cd "$1" || exit 1
ls *.wav | while read i; do
    md=$(ffprobe -i "$i" 2>&1);
    args=""
    for k in title album artist genre track date comment; do
        val=$(echo "$md" | awk -v key=$k '{
                regx = sprintf("^[ \t]+%s[ \t]+: ", key);
                n = split($0, a, regx);
                if (n > 1) print a[2];
            }'
        )
        eval $k="\$val"
        eval arg=\${${k}_arg}
        [ -n "$val" ] && args="$args $arg \"$val\""
    done
    eval lame --preset $preset $args \"$i\" \"${i%wav}mp3\"
done
 

roccobaroccoSC

Well-Known Member

Reaction score: 95
Messages: 425

This converts anything to .mp3 (depends on ffmpeg, and also mp3 encoding needs to be enabled when installing the port):
Bash:
% cat `which convertall2mp3 `
#!/bin/bash

find . -name "*.mp4" -exec convert2mp3 -i "{}" -o "{}.mp3" \;
find . -name "*.flv" -exec convert2mp3 -i "{}" -o "{}.mp3" \;
find . -name "*.wmv" -exec convert2mp3 -i "{}" -o "{}.mp3" \;

% cat `which convert2mp3 `   
#!/bin/bash

inputfile="";
outputfile="";
quality="192k";

starttime="";
duration="";

while [ "$1" != "" ]; do

  case $1 in
      "-i" )
          shift;
          inputfile=$1;
          shift;
          ;;
      "-o" )
          shift;
          outputfile=$1;
          shift;
          ;;
      "-q" )
          shift;
          quality=$1;
          shift;
          ;;
      "-ss" )
          shift;
          starttime="-ss $1";
          shift;
          ;;
      "-t" )
          shift;
          duration="-t $1";
          shift;
          ;;
      * )
          echo WARNING: Unknown parameter $1;
          shift;
          ;;
  esac;

done;

if [ "$inputfile" == "" ] || [ "$outputfile" == "" ]; then
  echo Usage: convert2mp3 -i inputfile -o outputfile [-q quality] [-ss starttime] [-t duration];
  echo ...... quality = 128k 160k 96k etc.;
  echo        ss = hh:mm:ss
  echo        t  = hh:mm:ss

else

  echo "Input file: $inputfile";
  echo "Output file: $outputfile";
  echo "Quality: $quality";

#  avconv -i "$inputfile" -acodec libmp3lame -ab $quality -ac 2 -ar 44100 "$outputfile";

  echo ffmpeg $starttime $duration -i "$inputfile" -b:a $quality -vn "$outputfile";
       ffmpeg $starttime $duration -i "$inputfile" -b:a $quality -vn "$outputfile";

fi;
 

NuLL3rr0r

Active Member

Reaction score: 16
Messages: 200

My Bash Script to Brighten up your FreeBSD box by wallpapers from Reddit

Documentation and more information https://www.babaei.net/blog/my-reddit-wallpaper-downloader-script/

Code:
#!/usr/bin/env bash

#  (The MIT License)
#
#  Copyright (c) 2018 - 2019 Mamadou Babaei
#
#  Permission is hereby granted, free of charge, to any person obtaining a copy
#  of this software and associated documentation files (the "Software"), to deal
#  in the Software without restriction, including without limitation the rights
#  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
#  copies of the Software, and to permit persons to whom the Software is
#  furnished to do so, subject to the following conditions:
#
#  The above copyright notice and this permission notice shall be included in
#  all copies or substantial portions of the Software.
#
#  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
#  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
#  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
#  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
#  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
#  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
#  THE SOFTWARE.


set +e

readonly FMT_OFF='\e[0m'
readonly FMT_INFO='\e[1;32m'
readonly FMT_WARN='\e[1;33m'
readonly FMT_ERR='\e[1;91m'
readonly FMT_FATAL='\e[1;31m'

readonly LOG_INFO="INFO"
readonly LOG_WARN="WARNING"
readonly LOG_ERR="ERROR"
readonly LOG_FATAL="FATAL"

readonly E_TRUE="true"
readonly E_FALSE="false"

readonly BASENAME="basename"
readonly CALLER="caller"
readonly CUT=$(which cut 2>/dev/null)
readonly CURL=$(which curl 2>/dev/null)
readonly DATE=$(which date 2>/dev/null)
readonly ECHO="echo"
readonly ECHO_FMT="echo -e"
readonly ESETROOT=$(which esetroot 2>/dev/null)
readonly FEH=$(which feh 2>/dev/null)
readonly FIREFOX=$(which firefox 2>/dev/null)
readonly HSETROOT=$(which hsetroot 2>/dev/null)
readonly JQ=$(which jq 2>/dev/null)
readonly LOGGER="logger"
readonly PERL=$(which perl 2>/dev/null)
readonly PRINT="print"
readonly REV=$(which rev 2>/dev/null)
readonly TR=$(which tr 2>/dev/null)
readonly XSETROOT=$(which xsetroot 2>/dev/null)

if [[ -n "${ESETROOT}" ]] ;
then
    readonly SETROOT=${ESETROOT}
elif [[ -n "${HSETROOT}" ]] ;
then
    readonly SETROOT=${HSETROOT}
elif [[ -n "${XSETROOT}" ]] ;
then
    readonly SETROOT=${XSETROOT}
fi

readonly SCRIPT="${BASH_SOURCE[0]}"
readonly SCRIPT_NAME="$(${BASENAME} -- "${SCRIPT}")"
readonly SYSLOG_TAG="$(${BASENAME} -- "${SCRIPT}" | ${TR} '[:lower:]' '[:upper:]' | ${REV} | ${CUT} -d "." -f2- | ${REV})"

readonly SUBREDDIT_CATEGORY_ANIMALS="AnimalsBeingBros+AnimalsBeingDerps+AnimalsBeingJerks+aww+Eyebleach+likeus+rarepuppers"
readonly SUBREDDIT_CATEGORY_ART="Art+ArtPorn+Cinemagraphs+ExposurePorn+Graffiti+ImaginaryLandscapes+itookapicture"
readonly SUBREDDIT_CATEGORY_FOOD="Breadit+eatsandwiches+food+FoodPorn+grilledcheese+Pizza+slowcooking"
readonly SUBREDDIT_CATEGORY_IMAGINARY="ImaginaryBehemoths+ImaginaryCharacters+ImaginaryLandscapes+ImaginaryLeviathans+ImaginaryMindscapes+ImaginaryMonsters+ImaginaryTechnology"
readonly SUBREDDIT_CATEGORY_MAN_MADE="AbandonedPorn+carporn+CityPorn+CozyPlaces+DesignPorn+powerwashingporn+RoomPorn"
readonly SUBREDDIT_CATEGORY_NATURE="chemicalreactiongifs+EarthPorn+MacroPorn+physicsgifs+spaceporn+waterporn+WeatherGifs"

readonly DEFAULT_SUBREDDIT="r/${SUBREDDIT_CATEGORY_ART}+${SUBREDDIT_CATEGORY_IMAGINARY}"
readonly DEFAULT_SORT_BY="hot"
readonly DEFAULT_IS_NSFW_OK="${E_FALSE}"
readonly DEFAULT_BACKGROUND_COLOR="282828"
readonly DEFAULT_FEH_ARGS="--no-fehbg --image-bg black --bg-max"

readonly DEFAULT_FIREFOX_VERSION_STRING="Mozilla Firefox 66.0"

readonly LOCAL_WALLPAPER_DIR="${HOME}/.cache/reddit_wallpapers/"
readonly LOCAL_WALLPAPER_NAME="$(${DATE} +%Y-%m-%d-%H-%M-%S)"

function usage() {
    readonly local message="${1}"

    ${ECHO}

    if [[ -n "${message}" ]] ;
    then
        err "${message}${FMT_OFF}"
        ${ECHO}
    fi

    ${ECHO_FMT} "${FMT_INFO}Correct usage:${FMT_OFF}"
    ${ECHO}
    ${ECHO_FMT} "    ${FMT_INFO}${SCRIPT_NAME} -h | [-r {r/subreddit}] [-s {sorty by}] [-n] [-b {background color}] [-f 'feh args']${FMT_OFF}"
    ${ECHO}
    ${ECHO_FMT} "    ${FMT_INFO}-h: shows this usage note"
    ${ECHO}
    ${ECHO_FMT} "    ${FMT_INFO}-r: subreddit name or names prefixed with r/ and combined by a + sign (e.g. r/Art or r/Art+ArtPorn; default: ${DEFAULT_SUBREDDIT})${FMT_OFF}"
    ${ECHO}
    ${ECHO_FMT} "    ${FMT_INFO}-s: reddit sort algorithm (e.g. hot, new, controversial, top, rising; default: ${DEFAULT_SORT_BY})${FMT_OFF}"
    ${ECHO}
    ${ECHO_FMT} "    ${FMT_INFO}-n: allow nsfw wallpapers (no nsfw wallpaper is allowed by default, unless this flag is passed)${FMT_OFF}"
    ${ECHO}
    ${ECHO_FMT} "    ${FMT_INFO}-b: hex rgb color in 'ffffff' format (default: ${DEFAULT_BACKGROUND_COLOR})${FMT_OFF}"
    ${ECHO}
    ${ECHO_FMT} "    ${FMT_INFO}-f: feh arguments to pass; run 'man feh' for a list of available options (default: ${DEFAULT_FEH_ARGS})${FMT_OFF}"
    ${ECHO}

    if [[ -n "${message}" ]] ;
    then
        exit 1
    else
        exit 0
    fi
}

function log()
{
    local log_type=$1; shift
    local line=$1; shift
    local fmt=$1; shift

    if [[ -n "$1" && -n "$@" ]] ;
    then
        ${ECHO_FMT} "${fmt}[${log_type}] ${line} $@${FMT_OFF}"
        ${LOGGER} -t "${SYSLOG_TAG}" "${log_type} ${line} $@"
    else
        ${ECHO_FMT} "${FMT_WARN}[${LOG_WARN}] ${line} A null log detected!${FMT_OFF}"
        ${LOGGER} -t "${SYSLOG_TAG}" "${LOG_WARN} ${line} A null log detected!"
    fi 
}

function info()
{
    local line=$( "${ECHO}" $( "${CALLER}" 0 ) | "${CUT}" -d " " -f1 )

    log "${LOG_INFO}" "${line}" "$FMT_INFO" "$@"
}

function warn()
{
    local line=$( "${ECHO}" $( "${CALLER}" 0 ) | "${CUT}" -d " " -f1 )

    log "${LOG_WARN}" "${line}" "${FMT_WARN}" "$@"
}

function err()
{
    local line=$( "${ECHO}" $( "${CALLER}" 0 ) | "${CUT}" -d " " -f1 )

    log "${LOG_ERR}" "${line}" "${FMT_ERR}" "$@"
}

function fatal()
{
    local line=$( "${ECHO}" $( "${CALLER}" 0 ) | "${CUT}" -d " " -f1 )

    log "${LOG_FATAL}" "${line}" "${FMT_FATAL}" "$@"

    exit 1
}

if [[ -z "${CUT}" ]] ;
then
    fatal "Could not find command cut!"
fi

if [[ -z "${CURL}" ]] ;
then
    fatal "Could not find command curl!"
fi

if [[ -z "${DATE}" ]] ;
then
    fatal "Could not find command date!"
fi

if [[ -z "${FEH}" ]] ;
then
    fatal "Could not find command feh!"
fi

if [[ -z "${JQ}" ]] ;
then
    fatal "Could not find command jq!"
fi

if [[ -z "${PERL}" ]] ;
then
    fatal "Could not find command perl!"
fi

if [[ -z "${REV}" ]] ;
then
    fatal "Could not find command rev!"
fi

if [[ -z "${SETROOT}" ]] ;
then
    fatal "Could not find any setroot command!"
fi

if [[ -z "${TR}" ]] ;
then
    fatal "Could not find command tr!"
fi

while getopts ":h :r: :s: :n :b: :f:" ARG;
do
    case ${ARG} in
        h)
            usage
            ;;
        r)
            SUBREDDIT=${OPTARG}
            ;;
        s)
            SORT_BY=${OPTARG}
            ;;
        n)
            IS_NSFW_OK="${E_TRUE}"
            ;;
        b)
            BACKGROUND_COLOR="#${OPTARG}"
            ;;
        f)
            FEH_ARGS="${OPTARG}"
            ;;
        \?)
            usage "Invalid option: '-${OPTARG}'!"
        ;;
    esac
done

if [[ -z ${SUBREDDIT} ]] ;
then
    SUBREDDIT=${DEFAULT_SUBREDDIT}
fi

readonly SUBREDDIT_REGEX="^r\/([a-zA-Z0-9_+]+)$"

if [[ ! "${SUBREDDIT}" =~ ${SUBREDDIT_REGEX} ]] ;
then
    fatal "Invalid subreddit name! use r/subreddit or r/subreddit1+subreddit2 format!"
fi

if [[ -z ${SORT_BY} ]] ;
then
    SORT_BY=${DEFAULT_SORT_BY}
fi

if [[ "${SORT_BY}" != "hot"
        && "${SORT_BY}" != "new"
        && "${SORT_BY}" != "controversial"
        && "${SORT_BY}" != "top"
        && "${SORT_BY}" != "rising" ]] ;
then
    fatal "Invalid reddit sort algorithm!"
fi

if [[ -z ${IS_NSFW_OK} ]] ;
then
    NSFW_OK=${DEFAULT_IS_NSFW_OK}
fi

if [[ -z ${BACKGROUND_COLOR} ]] ;
then
    BACKGROUND_COLOR="#${DEFAULT_BACKGROUND_COLOR}"
fi

readonly BACKGROUND_COLOR_REGEX="^#([0-9a-f]{6})$"

if [[ ! "${BACKGROUND_COLOR}" =~ ${BACKGROUND_COLOR_REGEX} ]] ;
then
    fatal "Invalid background color format! the only accepted format is 'ffffff'!"
fi

if [[ -z ${FEH_ARGS} ]] ;
then
    FEH_ARGS=${DEFAULT_FEH_ARGS}
fi

if [[ -z "${FIREFOX}" ]] ;
then
    warn "Firefox executable not found!"
    readonly FIREFOX_VERSION_STRING="${DEFAULT_FIREFOX_VERSION_STRING}"
    warn "Setting Firefox version string to: ${FIREFOX_VERSION_STRING}"
else
    readonly FIREFOX_VERSION_STRING=$(${FIREFOX} -version)
fi

readonly FIREFOX_VERSION_NUMBER=$(${ECHO} "${FIREFOX_VERSION_STRING}" | ${PERL} -nle "m/[-+]?([0-9]*\.[0-9]+|[0-9]+)/; ${PRINT} \$1")
readonly FIREFOX_USER_AGENET="Mozilla/5.0 (X11; Linux x86_64; rv:${FIREFOX_VERSION_NUMBER}) Gecko/20100101 Firefox/${FIREFOX_VERSION_NUMBER}"

info "Run '${SCRIPT_NAME} -h' for more information on available options."

info "Setting user agent to '${FIREFOX_USER_AGENET}'..."

readonly JSON_URL="https://www.reddit.com/${SUBREDDIT}/${SORT_BY}.json"

info "Downloading meta file '${JSON_URL}'..."

readonly JSON_CONTENT=$(${CURL} -A "${FIREFOX_USER_AGENET}" -sSL ${JSON_URL})
RC=$?

if [[ $RC -ne 0 ]] ;
then
    fatal "Subreddit meta file download has failed!"
fi

readarray JSON_POSTS <<< "$(${ECHO} "${JSON_CONTENT}" | ${JQ} --compact-output '.data.children[]')"
RCS=(${PIPESTATUS[*]})

if [[ ${RCS[0]} -ne 0 || ${RCS[1]} -ne 0 ]] ;
then
    fatal "Failed to parse the subreddit's meta file!"
fi

readonly URL_REGEX="^(https?://)?(([0-9a-z_!~*'().&=+$%-]+: )?[0-9a-z_!~*'().&=+$%-]+@)?(([0-9]{1,3}\\.){3}[0-9]{1,3}|([0-9a-z_!~*'()-]+\\.)*([0-9a-z][0-9a-z-]{0,61})?[0-9a-z]\\.[a-z]{2,6})(:[0-9]{1,4})?((/?)|(/[0-9a-z_!~*'().;?:@&=+$,%#-]+)+/?)\\.(png|apng|jpg|jpeg|jpe|jif|jfif|jfi|gif|tiff|tif)$"

FOUND_A_SUITABLE_WALLPAPER="${E_FALSE}"

for post in "${JSON_POSTS[@]}" ;
do
    if [[ "${IS_NSFW_OK}" != "${E_TRUE}" ]] ;
    then
        IS_OVER_18_POST=$(${ECHO} "${post}" | ${JQ} --compact-output '.data.over_18')
        RCS=(${PIPESTATUS[*]})

        if [[ ${RCS[0]} -ne 0 || ${RCS[1]} -ne 0 ]] ;
        then
            fatal "Failed to parse the subreddit's meta file!"
        fi

        if [[ "${IS_OVER_18_POST}" == "${E_TRUE}" ]] ;
        then
            continue
        fi
    fi

    WALLPAPER_URL=$(${ECHO} "${post}" | ${JQ} --raw-output '.data.url')
    RCS=(${PIPESTATUS[*]})

    if [[ ${RCS[0]} -ne 0 || ${RCS[1]} -ne 0 ]] ;
    then
        fatal "Failed to parse the subreddit's meta file!"
    fi

    if [[ ! "${WALLPAPER_URL}" =~ ${URL_REGEX} ]] ;
    then
        continue
    fi

    WALLPAPER_EXTENSION="${WALLPAPER_URL##*.}"

    if [[ -z "${WALLPAPER_EXTENSION}" ]] ;
    then
        continue
    fi

    FOUND_A_SUITABLE_WALLPAPER="${E_TRUE}"
    break;
done

if [[ "${FOUND_A_SUITABLE_WALLPAPER}" != "${E_TRUE}" ]] ;
then
    fatal "Could not find a suitable wallpaper on '${SUBREDDIT}'!"
fi

readonly LOCAL_WALLPAPER_PATH="${LOCAL_WALLPAPER_DIR}/${LOCAL_WALLPAPER_NAME}.${WALLPAPER_EXTENSION}"

info "Found a wallpaper on '${SUBREDDIT}' at '${WALLPAPER_URL}'!"
info "Fetching '${WALLPAPER_URL}'..."

${CURL} -fLo "${LOCAL_WALLPAPER_PATH}" --create-dirs ${WALLPAPER_URL} > /dev/null 2>&1
RC=$?

if [[ $RC -ne 0 ]] ;
then
    fatal "Failed to fetch the wallpaper file from '${WALLPAPER_URL}'!"
fi

info "Setting desktop background color to '${BACKGROUND_COLOR}'..."

${SETROOT} -solid "${BACKGROUND_COLOR}" > /dev/null 2>&1 &
RC=$?

if [[ $RC -ne 0 ]] ;
then
    fatal "Failed to set the background color to '${BACKGROUND_COLOR}'!"
fi

info "Using '${WALLPAPER_URL}' as the desktop wallpaper..."

${FEH} ${FEH_ARGS} "${LOCAL_WALLPAPER_PATH}" > /dev/null 2>&1 &
RC=$?

if [[ $RC -ne 0 ]] ;
then
    fatal "Failed to apply '${LOCAL_WALLPAPER_PATH}' as the desktop background image!"
fi

info "Done!"
info "Hope you enjoy it :)"
 

hukadan

Well-Known Member

Reaction score: 265
Messages: 414

Here is the script I use to update my system from source using boot environment. The building world and kernel part is a copy&paste of http://www.wonkity.com/~wblock/docs/html/buildworld.html. I am still learning shell scripting and struggling with proper error handling, so critics are more than welcome.
Code:
# !/bin/sh

set -eu

trap cleanup 2

MNT_POINT=$(mktemp -d /tmp/sysupgrade-XXXXXX) ||
    { echo "could not create mount point" ; exit 1 ;}
REVISION=$(svn info --show-item revision /usr/src) ||
    { echo "could not get revision number" ; exit 1 ;}
RELEASE=$(uname -r) ||
    { echo "could not get release" ; exit 1 ;}
BE_NAME=${RELEASE}-${REVISION}
JOB_NUMBER=7

create_be(){
    echo "==> creating boot environment"
    bectl create ${BE_NAME} ||
        { echo "could not create boot environment" ; exit 1 ;}
}

mount_be(){
    echo "==> mounting boot environment"
    bectl mount ${BE_NAME} ${MNT_POINT} ||
        { echo "could not mount boot environment" ; exit 1 ;}
    mount -t devfs devfs ${MNT_POINT}/dev ||
        { echo "could not mount devfs" ; exit 1 ;}
    mount_nullfs /usr/src ${MNT_POINT}/usr/src ||
        { echo "could not mount /usr/src" ; exit 1 ;}
}

umount_be(){
    echo "==> umounting boot environment"
    umount ${MNT_POINT}/dev ||
        { echo "could not umount devfs" ; exit 1 ;}
    umount ${MNT_POINT}/usr/src ||
        { echo "could not umount /usr/src" ; exit 1 ;}
    bectl umount ${BE_NAME} ||
        { echo "could not umount boot environment" ; exit 1 ;}
    rmdir ${MNT_POINT} ||
        { echo "could not delete mount point" ; exit 1 ;}
}

destroy_be(){
    echo "==> destroying boot environment"
    bectl destroy -o ${BE_NAME} ||
       { echo "could not destroy boot environment" ; exit 1 ;}
}

create_script(){
    cat << EOT > ${MNT_POINT}/script.sh
# !/bin/sh

echo "removing /usr/obj... this may take a while"
rm -rf /usr/obj ||
        { echo "coud not remove /usr/obj" ; exit 1 ;}
cd /usr/src ||
        { echo "couldn't reach /usr/src" ; exit 1 ;}
echo "building world"
make -j${JOB_NUMBER} buildworld ||
        { echo "buildworld failed" ; exit 1 ;}
echo "building and installing kernel"
make -j${JOB_NUMBER} kernel ||
        { echo "kernel failed" ; exit 1 ;}
echo "intalling world"
make installworld ||
        { echo "installworld failed" ; exit 1 ;}
echo "merging files"
mergemaster -Ui
echo "checking old"
make check-old
echo "deleting old"
make delete-old
echo "delete old libs"
make delete-old-libs
echo "removing install script"
rm /script.sh ||
        { echo "install script could not be removed" ; exit 1 ;}
EOT
    return 0
}

chroot_exec(){
    echo "==> chrooting"
    chroot ${MNT_POINT} /bin/sh script.sh
}

cleanup(){
    echo "==> cleaning"
    umount_be
    destroy_be
}

upgrade(){
    create_be
    mount_be
    create_script
    chroot_exec
    umount_be
}

upgrade
 

NuLL3rr0r

Active Member

Reaction score: 16
Messages: 200

My script for keeping the crashing daemons running.

A blog post explaining it.

Code:
#!/usr/bin/env sh

#  (The MIT License)
#
#  Copyright (c) 2019 Mamadou Babaei
#
#  Permission is hereby granted, free of charge, to any person obtaining a copy
#  of this software and associated documentation files (the "Software"), to deal
#  in the Software without restriction, including without limitation the rights
#  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
#  copies of the Software, and to permit persons to whom the Software is
#  furnished to do so, subject to the following conditions:
#
#  The above copyright notice and this permission notice shall be included in
#  all copies or substantial portions of the Software.
#
#  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
#  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
#  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
#  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
#  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
#  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
#  THE SOFTWARE.


set +e

readonly BASENAME="basename"
readonly CUT="/usr/bin/cut"
readonly ECHO="echo -e"
readonly GREP="/usr/bin/grep"
readonly LOGGER="/usr/bin/logger"
readonly PS="/bin/ps"
readonly REV="/usr/bin/rev"
readonly TR="/usr/bin/tr"

readonly FMT_OFF='\e[0m'
readonly FMT_INFO='\e[1;32m'
readonly FMT_WARN='\e[1;33m'
readonly FMT_ERR='\e[1;91m'
readonly FMT_FATAL='\e[1;31m'

readonly LOG_INFO="INFO"
readonly LOG_WARN="WARNING"
readonly LOG_ERR="ERROR"
readonly LOG_FATAL="FATAL"

readonly SCRIPT="$0"
readonly SCRIPT_NAME="$(${BASENAME} -- "${SCRIPT}")"
readonly SYSLOG_TAG="$(${BASENAME} -- "${SCRIPT}" \
    | ${TR} '[:lower:]' '[:upper:]' \
    | ${REV} \
    | ${CUT} -d "." -f2- \
    | ${REV})"

usage()
{
    ${ECHO}
    ${ECHO} "${FMT_INFO}Correct usage:${FMT_OFF}"
    ${ECHO}
    ${ECHO} "    ${FMT_INFO}${SCRIPT_NAME} -e {executable full path} -s {service name to (re)start} [-s {another service name to (re)start}] [... even more -s and service names to (re)start]${FMT_OFF}"
    ${ECHO}

    exit 1
}

log()
{
    log_type=$1; shift
    fmt=$1; shift

    if [ -n "$1" -a -n "$@" ] ;
    then
        ${ECHO} "${fmt}[${log_type}] $@${FMT_OFF}"
        ${LOGGER} -t "${SYSLOG_TAG}" "${log_type} $@"
    fi 
}

info()
{
    log "${LOG_INFO}" "${FMT_INFO}" "$@"
}

warn()
{
    log "${LOG_WARN}" "${FMT_WARN}" "$@"
}

err()
{
    log "${LOG_ERR}" "${FMT_ERR}" "$@"
}

fatal()
{
    log "${LOG_FATAL}" "${FMT_FATAL}" "$@"
    exit 1
}

restart_service()
{
    service_name="$1"

    info "Stopping the service '${service_name}'..."
    service ${service_name} stop > /dev/null 2>&1

    if [ "$?" -eq 0 ] ;
    then
        info "The '${service_name}' service has been stopped successfully!"
    else
        err "Failed to stop the '${service_name}' service!"
    fi

    info "Starting the service '${service_name}'..."
    service ${service_name} start > /dev/null 2>&1

    if [ "$?" -eq 0 ] ;
    then
        info "The '${service_name}' service has been started successfully!"
    else
        err "Failed to start the '${service_name}' service!"
    fi
}

if [ "$#" -eq 0 ] ;
then
    usage
fi

SERVICE_COUNT=0

while getopts ":e: :s:" ARG ;
do
    case ${ARG} in
        e)
            if [ -z "${OPTARG}" ] ;
            then
                err "Missing executable ${OPTARG}!"
                usage
            fi

            if [ ! -f "${OPTARG}" ] ;
            then
                fatal "The executable '${OPTARG}' does not exist!"
            fi

            readonly DAEMON="${OPTARG}"
            ;;
        s)
            if [ ! -f "/usr/etc/rc.d/${OPTARG}" \
                -a ! -f "/usr/local/etc/rc.d/${OPTARG}" ] ;
            then
                fatal "No such a service exists: '${OPTARG}'!"
            fi

            SERVICE_COUNT=$((SERVICE_COUNT+1))
            ;;
        \?)
            err "Invalid option: -${OPTARG}!"
            usage
        ;;
    esac
done

if [ "${SERVICE_COUNT}" -eq 0 ] ;
then
    err "At least one service name is required!"
    usage
fi

readonly DAEMON_PROCESS_COUNT=$(${PS} aux \
    | ${GREP} -v "${GREP}" \
    | ${GREP} -v "${SCRIPT}" \
    | ${GREP} -c "${DAEMON}")

if [ "${DAEMON_PROCESS_COUNT}" -lt 1 ] ;
then
    warn "'${DAEMON}' is not running!"

    OPTIND=1
    while getopts ":e: :s:" ARG ;
    do
        case ${ARG} in
            s)
                restart_service "${OPTARG}"
                ;;
            \?)
            ;;
        esac
    done
else
    info "'${DAEMON}' is running!"
    info "No action is required!"
fi
 

kpedersen

Daemon

Reaction score: 512
Messages: 1,385

I was on the train to Glasgow the other day; I brought my old Thinkpad T23 running MS-DOS to play some DOS games for entertainment. I decided to do a bit of coding but remembered that CMake has not been ported to DOS (yet?). So decided to make a new build system using a tool that is present on every development machine I use... Vim!

Save it in a file (i.e iffe.vim) You can either make it executable and run it, or you can run it with vim -S iffe.vim

Some features:
  • Pure 100% unadulterated VimScript
  • Fast! (due to lack of features ;))
  • Dependency checking (including recursive header searching)
  • Logic does not use recursion so it can work on... DOS! (open / closed list similar to A* used instead)
  • It requires no build file, instead it works out based on the filesystem structure how the project should be built. i.e

Code:
projname/ <--- you are here
  iffe.vim
  src/
    somelib/
      Test.cpp
      Test.h
    somebin/
      main.cpp
This will then generate a bunch of .o files in the obj folder, a libsomelib.a static library in the lib folder and somebin executable in the bin folder. It will also store the dependencies database in obj/dependencies.iffe for subsequent builds.

Unfortunately the trip was over before I managed to do any actual coding XD

Code:
#!/usr/local/bin/vim -S

" BSD / LLVM
let s:compiler = "clang++"
let s:objSuffix = "o"
let s:exeSuffix = ""

" DOS / DJGPP
"let s:compiler = "g++"
"let s:objSuffix = "o"
"let s:exeSuffix = "exe"

"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
" Output
"
" Write the specified message to the buffer using normal mode and then request
" a redraw to ensure that the text is visible even if further processing is
" needing to be done.
"
" message - The message to add to the buffer
"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
function Output(message)

  execute("normal! Ga" . a:message . "\n")
  redraw!

endfunction

"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
" FileCreate
"
" Based on the specified path, populate a structure for the relevent file.
" The data to be populated includes information on whether it is a header or
" source unit. If a source unit, then generate a path to be used for the
" compiled object. If the file is called "main" then set the executable flag
" in the parent project so it is known to not just be a library.
"
" project - The parent project of this file
" path - The relative file path
"
" Returns - The populated file structure
"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
function FileCreate(project, path)

  let l:ctx = {}
  let l:ctx.project = a:project
  let l:ctx.path = a:path
  let l:ctx.name = fnamemodify(l:ctx.path, ':t')
  let l:ctx.absolutepath = fnamemodify(l:ctx.path, ':p')
  let l:ctx.timestamp = getftime(l:ctx.path)
  let l:ctx.dependencies = []

  if fnamemodify(l:ctx.path, ':e') == "cpp"
    let l:ctx.objpath = fnamemodify(l:ctx.path, ':t:r')

    if l:ctx.objpath == "main"
      let a:project.executable = 1
    endif

    let l:objpath = "obj/" . l:ctx.project.name . "/"
    let l:objpath = l:objpath . l:ctx.objpath . "." . s:objSuffix
    let l:ctx.objpath = l:objpath
  else
    let l:ctx.objpath = ""
  endif

  return l:ctx

endfunction

"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
" DepParserCreate
"
" Create a structure containing the necessary data store for the dependency
" parser. In particular create two arrays referring to the open and closed
" list and prepopulate the open list with the initial source unit to scan.
"
" file - The file to pre-populate the open list with
"
" Returns - The created parser context
"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
function DepParserCreate(file)

  let ctx = {}
  let ctx.iffe = a:file.project.iffe
  let ctx.open = []
  call add(ctx.open, a:file)
  let ctx.closed = []

  return ctx

endfunction

"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
" DepParserProcess
"
" Read the required files and generate a list of all included dependencies
" recursively. The method itself is not recursive (to reduce the required
" stack size on weaker platforms such as DOS) and as such uses an open / closed
" list system (in a similar way to path finding). By the end of the process the
" closed list within the context will contain the initial files and all of
" their dependencies.
"
" ctx - The parser context
"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
function DepParserProcess(ctx)

  while len(a:ctx.open) > 0
    let l:file = a:ctx.open[0]
    call remove(a:ctx.open, 0)
    call add(a:ctx.closed, l:file)

    let l:data = readfile(l:file.path)

    for l:line in l:data
      if l:line =~ "^#include"
        let l:line = substitute(l:line, '"\s*$', "", "")
        let l:line = substitute(l:line, '>\s*$', "", "")
        let l:line = substitute(l:line, '^.*"', "", "")
        let l:line = substitute(l:line, '^.*<', "", "")

        let l:inc = fnamemodify(l:line, ':t')
        let l:found = 0

        for l:ent in a:ctx.open
          if l:ent.name == l:inc
            let l:found = 1
            break
          endif
        endfor

        if l:found == 0
          for l:ent in a:ctx.closed
            if l:ent.name == l:inc
              let l:found = 1
              break
            endif
          endfor
        endif

        if l:found == 0
          for l:ent in a:ctx.iffe.headers
            if l:ent.name == l:inc
              call add(a:ctx.open, l:ent)
            endif
          endfor
        endif
      endif
    endfor

  endwhile

endfunction

"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
" SourceUpdateDependencies
"
" Update all the dependencies of the specified source file by creating an
" instance of the file parser, adding any source file with the specified file's
" name, and processing it. All files in the closed list which are header files
" are copied into the source as dependencies (replacing the existing list).
"
" ctx - The source file to update
"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
function SourceUpdateDependencies(ctx)

  let l:dp = DepParserCreate(a:ctx)

  for l:src in a:ctx.project.iffe.sources
    if l:src == a:ctx
      continue
    endif

    if l:src.name == a:ctx.name
      call add(l:dp.open, l:src)
    endif
  endfor

  call DepParserProcess(l:dp)
  let a:ctx.dependencies = []

  for l:dep in l:dp.closed
    if l:dep.objpath == ""
      call add(a:ctx.dependencies, l:dep)
    endif
  endfor

endfunction

"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
" SourceBuild
"
" Build the specified source file using its path and its generated object path.
" The "obj" folders for the files project are created if they do not exist.
" This runs the current compiler with the correct arguments. Finally update
" the dependencies of the file because we now have an up to date object but the
" included files might change in future.
"
" ctx - The source unit to compile into an object
"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
function SourceBuild(ctx)

  "call Output("Building: " . a:ctx.name)
  "call Output("for Project: " . a:ctx.project.name)

  if !isdirectory("obj")
    call mkdir("obj")
  endif

  if !isdirectory("obj/" . a:ctx.project.name)
    call mkdir("obj/" . a:ctx.project.name)
  endif

  let l:cmd = s:compiler . " -c -o " . a:ctx.objpath . " -Isrc " . a:ctx.path
  call Output(l:cmd)
  let l:output = system(l:cmd)

  if l:output != ""
    call Output(l:output)
  endif

  call SourceUpdateDependencies(a:ctx)

endfunction

"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
" ProjectCreate
"
" Populate the project structure based on the details of the specified path.
" Scan the respective directory (not recursively) for any source and header
" files and add them to the correct list. Based on if any of those files were
" called "main" the executable flag will be set and generate a correct output
" filename for this project which will be used when building.
"
" iffe - The encapsulating parent system
" path - The relative path of the project
"
" Returns - The populated project structure
"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
function ProjectCreate(iffe, path)

  let l:ctx = {}
  let l:ctx.path = a:path
  let l:ctx.name = fnamemodify(l:ctx.path, ':t')
  let l:ctx.absolutepath = fnamemodify(l:ctx.path, ':p:h')
  let l:ctx.sources = []
  let l:ctx.headers = []
  let l:ctx.executable = 0
  let l:files = globpath(l:ctx.path, '*')
  let l:files = split(l:files, '\n')

  for l:path in l:files

    if getftype(l:path) != "file"
      continue
    endif

    let l:file = FileCreate(l:ctx, l:path)

    if l:file.objpath == ""
      call add(l:ctx.headers, l:file)
      call add(a:iffe.headers, l:file)
    else
      call add(l:ctx.sources, l:file)
      call add(a:iffe.sources, l:file)
    endif

  endfor

  let l:suff = s:exeSuffix

  if l:suff != ""
    let l:suff = "." . l:suff
  endif

  if l:ctx.executable == 1
    let l:ctx.outpath = "bin/" . l:ctx.name . l:suff
  else
    let l:ctx.outpath = "lib/lib" . l:ctx.name . ".a"
  endif

  return l:ctx

endfunction

"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
" SourceRequiresBuild
"
" Ascertain whether the specified source unit requires a rebuild. This is done
" by checking if the timestamp of the file is more recent than the existing
" object or if any of its dependencies have a more recent timestamp.
"
" ctx - The source file to query
"
" Returns - 1 if the source requires a rebuild or 0 otherwise
"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
function SourceRequiresBuild(ctx)

  " TODO: Testing
  "return 1

  let l:objtimestamp = getftime(a:ctx.objpath)

  if a:ctx.timestamp > l:objtimestamp
    return 1
  endif

  for l:dep in a:ctx.dependencies
    if l:dep.timestamp > l:objtimestamp
      return 1
    endif
  endfor

  return 0

endfunction

"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
" ProjectBuild
"
" All the sources belonging to the project are built. If any of the resulting
" objects are newer than the relevant executable or library file the project
" output is linked into an executable or archived into a library depending on
" the type. The "bin" or "lib" directory is created if it does not already
" exist.
"
" ctx - The specified project to build
"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
function ProjectBuild(ctx)

  "call Output("Processing: " . a:ctx.name)
  let l:objstr = ""
  let l:newest = -1

  for l:source in a:ctx.sources

    if SourceRequiresBuild(l:source)
      call SourceBuild(l:source)
    endif

    let l:objstr = l:objstr . l:source.objpath . " "
    let l:objts = getftime(l:source.objpath)

    if l:objts > l:newest
      let l:newest = l:objts
    endif

  endfor

  if getftime(a:ctx.outpath) >= l:newest
    return
  endif

  if a:ctx.executable == 1

    if !isdirectory("bin")
      call mkdir("bin")
    endif

    let l:cmd = s:compiler . " -o " . a:ctx.outpath . " " . l:objstr
    call Output(l:cmd)
    let l:output = system(l:cmd)

    if l:output != ""
      call Output(l:output)
    endif
  else
    if !isdirectory("lib")
      call mkdir("lib")
    endif

    let l:cmd = "ar rcs " . a:ctx.outpath . " " . l:objstr
    call Output(l:cmd)
    let l:output = system(l:cmd)

    if l:output != ""
      call Output(l:output)
    endif
  endif

  call Output("")

endfunction

"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
" IffePopulateProjects
"
" Scan through the "src" directory for any folders. Assume that these are
" projects so process them and add them to the list.
"
" ctx - The encapsulated system
"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
function IffePopulateProjects(ctx)

  let a:ctx.projects = []
  let a:ctx.sources = []
  let a:ctx.headers = []

  let l:files = globpath("src", '*')
  let l:files = split(l:files, '\n')

  for l:file in l:files
    if getftype(l:file) != "dir"
      continue
    endif

    let l:project = ProjectCreate(a:ctx, l:file)
    let l:project.iffe = a:ctx
    call add(a:ctx.projects, l:project)
  endfor

endfunction

"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
" IffeRestoreDependencies
"
" If the dependencies file exists, read through it whilst matching any source
" files with a name from the file. If any files match go through the list of
" dependencies from the file and if any header files match these names, then
" add them to the source file as a dependency. As a redundant check in case
" there are duplicate entries (there should not be), create a hash of any new
" dependencies and source and add it to a list to be subsequently checked to
" ensure that a source file does not receive the same dependency twice (same
" file name is fine so long as the path is different).
"
" ctx - The encapsulated system
"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
function IffeRestoreDependencies(ctx)

  let l:added = []

  if !filereadable("obj/dependencies.iffe")
    return
  endif

  let l:lines = readfile("obj/dependencies.iffe")

  for l:line in l:lines
    let l:source = substitute(l:line, ': .*$', "", "")
    let l:dstr = substitute(l:line, '.*: ', "", "")
    let l:deps = split(l:dstr, ' ')

    "call Output("Deps: " . l:source)

    "for l:dep in l:deps
    "  call Output("Dep: " . l:dep)
    "endfor

    for l:s in a:ctx.sources
      if l:s.name != l:source
        continue
      endif

      "call Output("Source: " . l:s.name)

      for l:dep in l:deps
        for l:d in a:ctx.headers
          if l:d.name == l:dep

            let l:found = 0

            for l:a in l:added
              if l:a == l:s.path . " " . l:d.path
                let l:found = 1
                break
              endif
            endfor

            if l:found == 0
              "call Output("Source: " . l:s.name . " Dep: " . l:d.path)
              call add(l:added, l:s.path . " " . l:d.path)
              call add(l:s.dependencies, l:d)
            endif

          endif
        endfor
      endfor
    endfor
  endfor

endfunction

"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
" IffeCreate
"
" Output a small banner, create the encapsulated system structure and populate
" it with projects.
"
" Returns - The encapsulated system structure
"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
function IffeCreate()

  let l:ctx = {}

  call Output("Iffe Build System")
  call Output("-----------------")

  call IffePopulateProjects(l:ctx)
  call IffeRestoreDependencies(l:ctx)

  return l:ctx

endfunction

"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
" IffeStoreDependencies
"
" For all of the sources, iterate through all of their dependencies and output
" their filenames into the dependencies file. The main thing to note is that
" a source with the same name but entirely different paths will actually share
" the same dependencies (to keep things simple and not depend on paths), so it
" is only necessary to output one of them. For this reason a list is created
" to store the processed sources and only the first time a source with a given
" name is encountered is it added to the file. The dependencies file is stored
" in the "obj" folder which is created if it does not already exist.
"
" ctx - The encapsulated system
"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
function IffeStoreDependencies(ctx)

  let l:lines = []
  let l:seen = []

  for l:file in a:ctx.sources
    let l:out = "" . l:file.name . ":"
    let l:done = []
    let l:srcfound = 0

    for l:s in l:seen
      if l:s == l:file.name
        let l:srcfound = 1
        break
      endif
    endfor

    if l:srcfound == 1
      continue
    endif

    call add(l:seen, l:file.name)

    if len(l:file.dependencies) > 0
      for l:dep in l:file.dependencies
        let l:found = 0

        for l:d in l:done
          if l:d == l:dep.name
            let l:found = 1
            break
          endif
        endfor

        if l:found == 0
          let l:out = l:out . " " . l:dep.name
          call add(l:done, l:dep.name)
        endif
      endfor

      "call Output(l:file.name)
      "call Output(l:out)
      call add(l:lines, l:out)
    endif

  endfor

  if !isdirectory("obj")
    call mkdir("obj")
  endif

  call writefile(l:lines, "obj/dependencies.iffe", "b")

endfunction

"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
" IffeClean
"
" Recursively delete the "bin", "lib" and "obj" folders if any of them exist.
"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
function IffeClean()

  if isdirectory("bin")
    call delete("bin", "rf")
  endif

  if isdirectory("lib")
    call delete("lib", "rf")
  endif

  if isdirectory("obj")
    call delete("obj", "rf")
  endif

endfunction

"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
" IffeBuild
"
" Iterate through all of the library projects and build them, then do the same
" for the executable projects. Finally store the updated dependencies from the
" source files into the database.
"
" ctx - The encapsulating system
"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
function IffeBuild(ctx)

  for l:project in a:ctx.projects
    if l:project.executable == 0
      call ProjectBuild(l:project)
    endif
  endfor

  for l:project in a:ctx.projects
    if l:project.executable == 1
      call ProjectBuild(l:project)
    endif
  endfor

  call IffeStoreDependencies(a:ctx)

endfunction

"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
" IffeOptions
"
" Output a number of post build options and map them to buffer specific key
" presses.
"
" ctx - The encapsulating system
"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
function IffeOptions(ctx)

  call Output("-----------------")
  call Output("Press 'r' to rebuild")
  call Output("Press 'c' to clean")
  call Output("Press 'q' to exit")
  nnoremap <buffer> q :q!<CR>
  nnoremap <buffer> r :call IffeMain()<CR>
  nnoremap <buffer> c :call IffeClean()<CR>

endfunction

"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
" IffeMain
"
" Clear the buffer (in case it is a subsequent run), create the encapsulating
" structure and start the main processes.
"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
function IffeMain()

  execute("normal! ggdG")

  let l:iffe = IffeCreate()
  call IffeBuild(l:iffe)
  call IffeOptions(l:iffe)

endfunction

call IffeMain()
Edit: Also uploaded to GitHub: https://github.com/osen/iffe.vim
I am probably going to keep working on it because it is really handy for platforms that CMake doesn't really deal well with such as Megadrive / GBA SDK, Emscripten, Android NDK compilers.
 
Top