Useful scripts

Dumping a little helper for sysutils/vm-bhyve I created today, adding a possibility for a "post-startup" script in a horribly hacky way ? ... placed in /var/vm/poststartup.sh

Bash:
#!/bin/sh

vm_script=/var/vm/${1}/poststartup.sh
vm_logfile=/var/vm/${1}/vm-bhyve.log

vm_log()
{
  if [ -w ${vm_logfile} ]; then
    LANG=C echo "`date '+%b %d %H:%M:%S:'` $@" >>${vm_logfile}
  fi
}

if [ -r ${vm_script} ]; then (
  sleep 1;

  for i in 1 2 3 4 5 6 7 8; do
    vm_status=`vm list | grep "^${1} .* Running ("`
    if [ -n "${vm_status}" ]; then
      vm_pid=`echo "${vm_status}" | sed -e 's:.*Running (\([0-9]*\).*:\1:'`
      vm_log poststartup.sh: Running ${vm_script}.
      . ${vm_script}
      exit
    else
      sleep 1
    fi
  done
  vm_log poststartup.sh: Giving up, vm is not running!
) </dev/null >/dev/null 2>&1 &
else
  vm_log poststartup.sh: ${vm_script} is missing!
fi

To use it, just add prestart="/var/vm/poststartup.sh" to your vm config and place an individual poststartup.sh in its directory. E.g. I have the following for my router/firewall vm, so that routing is never ever slowed down or killed by OOM:

Code:
protect -p ${vm_pid}
rtprio 15 -${vm_pid}
 
why did you use () instead of {} for command grouping ?
Dumping a little helper for sysutils/vm-bhyve I created today, adding a possibility for a "post-startup" script in a horribly hacky way ? ... placed in /var/vm/poststartup.sh

Bash:
#!/bin/sh

vm_script=/var/vm/${1}/poststartup.sh
vm_logfile=/var/vm/${1}/vm-bhyve.log

vm_log()
{
  if [ -w ${vm_logfile} ]; then
    LANG=C echo "`date '+%b %d %H:%M:%S:'` $@" >>${vm_logfile}
  fi
}

if [ -r ${vm_script} ]; then (
  sleep 1;

  for i in 1 2 3 4 5 6 7 8; do
    vm_status=`vm list | grep "^${1} .* Running ("`
    if [ -n "${vm_status}" ]; then
      vm_pid=`echo "${vm_status}" | sed -e 's:.*Running (\([0-9]*\).*:\1:'`
      vm_log poststartup.sh: Running ${vm_script}.
      . ${vm_script}
      exit
    else
      sleep 1
    fi
  done
  vm_log poststartup.sh: Giving up, vm is not running!
) </dev/null >/dev/null 2>&1 &
else
  vm_log poststartup.sh: ${vm_script} is missing!
fi

To use it, just add prestart="/var/vm/poststartup.sh" to your vm config and place an individual poststartup.sh in its directory. E.g. I have the following for my router/firewall vm, so that routing is never ever slowed down or killed by OOM:

Code:
protect -p ${vm_pid}
rtprio 15 -${vm_pid}
 
covacat That's not for grouping, I need the subshell (running in background) here, otherwise I'd block the parent preventing it from actually starting the vm, defeating the purpose of the script ?
 
last day of month
date -v -1d -j $(date -v +1m +%Y%m010000) +%d
or say you want to run a cron job in the last day of the month
Code:
#!/bin/sh
[ $(date -v +1d +%m) != $(date  +%m) ] || exit 0
# actual stuff here
Hmm. Today is the last day of the month if tomorrow is a first. That's one less invocation of date:

if test $(date -v +1d +%d) = 01; then; ...; fi
 
Secure Subversion (via svnserve)

Though git tends to be more popular these days, one of the best aspects of Subversion is the standalone server (svnserve) with simple authentication and virtual accounts making it suitable for small to medium teams. Unfortunately traffic (minus passwords) are sent in plain text. One solution is SASL which is fiddly and pulls in a load of random dependencies. Another solution is simply wrapping it all via OpenSSL (and svnserve via stunnel). The following is a quick overview how to do it:

ssvn (svn wrapper)
Code:
#!/bin/sh

set -e

if [ -n "$SVN_TUNNEL" ]; then
  exec openssl s_client -quiet -connect "$1:3691" 2>/dev/null
fi

export SVN_TUNNEL=1

exec svn \
  --no-auth-cache \
  --config-option config:tunnels:ssl="$0" \
  "$@"

stunnel.conf (i.e $ stunnel /path/to/stunnel.conf)
Code:
foreground = yes

[svn]
accept = 3691
connect = 3690
cert = /path/to/random/self/signed.pem

With this in place, you can now access repositories securely using svn+ssl://. For example:

$ ssvn co svn+ssl://example.com/myproj/trunk myproj
 
what a cool topic!!!

I have an script for taking zfs snapshots and sending to an backup HD that are not mounted. The script mounts the partition, and sends the snapshots as backups:


sh:
#!/bin/sh

new_snapshot="zroot@$(date +%d-%m-%Y)"
log_file="/var/log/backup.log"

send_not_script="/root/.dotfiles/scripts/send_notifications"

log_with_timestamp() {
    echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" >> "$log_file"
}

send_notification() {
    message="$1"
    sh $send_not_script "ZFS Backup" "$message" # Call the send_not script with the message
}

attach_backup_pool() {
    geli_partition="/dev/ada0p2"
    geli_keyfile="/root/keys/ada0p2.key"
    echo | geli attach -k $geli_keyfile -j - $geli_partition
    poolname=$(zpool import | grep "pool:" | awk '{print $2}')
    if [ -z "$(zpool list | grep $poolname)" ]; then
        zpool import -f $poolname
    fi
}

log_with_timestamp "Attaching Backup Pool"
send_notification "Attaching Backup Pool"
attach_backup_pool

log_with_timestamp "Creating new snapshot: $new_snapshot"
zfs snapshot -r "$new_snapshot" >> "$log_file" 2>> "$log_file"

if [ $? -eq 0 ]; then
    log_with_timestamp "Snapshot $new_snapshot created successfully."
    send_notification "Snapshot $new_snapshot created successfully."
   
    log_with_timestamp "Sending snapshot $new_snapshot to backup pool."

    zfs send -R "$new_snapshot" > /backup/"$new_snapshot"
    if [ $? -eq 0 ]; then
        log_with_timestamp "Successfully sent $new_snapshot to backup pool."
        send_notification "Successfully sent $new_snapshot to backup pool."
    else
        log_with_timestamp "Error sending $new_snapshot to backup pool."
        send_notification "Error sending $new_snapshot to backup pool."
    fi
else
    log_with_timestamp "Error creating snapshot $new_snapshot."
    send_notification "Error creating snapshot $new_snapshot."
fi

if [ ! -z "$(zpool list | grep $poolname)" ]; then
    zpool export $poolname
fi

geli detach $geli_partition

send notification:


sh:
#!/bin/sh

tittle="$1"
message="$2"
user="base"
export DISPLAY=":0"
doas -u $user env DISPLAY=$DISPLAY notify-send "$tittle" "$message"


the funny part is that I never tried to revert or actualy see if I can recover a system from the snapshots. But this makes me feel safe anyways.
 
Made a devd rule to have my joystick move to /dev/uhid# automatically for FlightGear:

Code:
/usr/local/etc/devd/sidewinder-precision-pro-joystick-uhidd.conf

Code:
notify 0 {
        match "type" "ATTACH";
        match "ugen" "ugen[0-9]+.[0-9]+";
        match "vendor" "0x045e";
        match "product" "0x0008";
        action "/usr/local/sbin/uhidd -h -H uhid -u /dev/$ugen";
};
 
This is a script I wrote to take care of the arduous task of updating the ssh key of a single machine on some the machines listed in the variable, HOSTS below. Comments and suggestions are welcome.
I’m not sure whether this was part of a FreeBSD installation in 2009, but today you can use ssh-copy-id(1). Moreover, it prevents duplicate entries, too. ?️‍♀️
Some backup tools have problems with long names. So we have to check the length of the path + file name. This can be done with: […]
Depending on your requirements pathchk(1) might be just the right tool for you. ?​
Bash:
pathchk -p '/some/path/to/check' # -p stands for portability check
Some of people want to ping some hosts and find which host is up […]
Consider setting up rwhod(8) instead. ?️​
This night I was compiling LibreOffice. By one o'clock it still wasn't finished.
I didn't want computer to stay up all night, so I wrote this little script: […]
Bash:
pwait $(pgrep make) && shutdown -p now # replace `make` as appropriate☻
I would only change that above into that below: […]
[…] Yes, that seems stupid running /usr/bin/basename twelve times instead of one. […]
You use basename(1) if you know nothing about the value’s structure (read user input). ?‍♀️ We know about $0 though that it successfully led to reading our shell script, so it is a valid pathname to a regular file, and thus using parameter expansion is sufficient; no spawning of new processes necessary.​
Bash:
printf 'My name is %s.\n' "${0##*/}" # `##` means remove largest prefix
[…] Please, post here useful scripts which can help to do some useful or difficult operation on FreeBSD OS. […]
Yeah, the above comments exemplify sometimes it’s just ignorance. The difficultly really lies in knowing your way around. ?️ And I certainly scripted some redundant stuff, too.​
Here is a very simple little script that I use to reduce the size of pictures. […]
It may be worth pointing out any prerequisites; in your particular script not everyone necessarily knows convert(1) is part of graphics/ImageMagick. ?​
In bash I use alias recall='history | egrep' so that I can do something like recall ssh. […]
And if you set HISTIGNORE='recall *' it does not even pollute the history. ☸️
admin script... […]
admin(1) is the name of a POSIX™‐standardized utility from devel/sccs, so maybe it’s not the best choice. ?​
[…] Sometimes it's difficult to locate a particular directory in a long $PATH variable. […]
$ echo $PATH | tr ':' ' ' | xargs -n 1 echoYou can probably write this even shorter
How about
% echo $PATH | tr ':' '\n'
Neat, yet this implementation does not take account of the possibility that $PATH members may be empty strings. ?‍⬛ An empty string implicilty denotes the current working directory; concededly, this notation is a legacy feature.​
Here's better one
$ echo $PATH | awk 'BEGIN {RS=":"}; {print $0}'

this one will respect spaces in directories
I always get a blank line at the end. Considering that an empty string is a valid $PATH specification, this is a bug. ? My take on the task:​
Bash:
#!/bin/sh -u
#        name: print path
# description: list values of `${PATH}` and highlight current working directory
#  maintainer: Firstname Lastname <mailbox@host> # in multi‐sysadmin environment

# Reset graphics rendition selection.
# `2>&-` removes any complaint should `tput` not be accessible via `${PATH}`.
[ -t 1 ] && tput sgr0 2>&-
# We presume this script never gets `source`d by another script.
IFS=':'
# The empty strings are necessary so a leading or trailing `:`
# is regarded as a word _splitter_, i. e. a character _between_ two words.
for path in ''${PATH:?Empty \$PATH has implementation-defined behavior.}''
do
    # Enumerating items is helpful
    # if there are too few `tput lines` to display everything on one screen.
    printf '%2d. ' "${list_item_number:=1}"
    list_item_number=$((${list_item_number} + 1))
   
    # Print current working directory in bold (if supported).
    [ -t 1 ] && [ "${path%.}"              ] || tput bold 2>&-
    # Some people like to indicate directories with a trailing slash.
    [ -t 1 ] && [ "${path%/}" = "${PWD%/}" ] && tput bold 2>&-
   
    # Empty string implicitly means current working directory.
    printf '%s\n' "${path:-(current working directory)}"
    # The last command in a loop determines the entire loop’s exit status.
    # If `tput` is not present in `${PATH}`, this will be non‐zero.
    # We want to report success, though, hence the null command `:`.
    [ -t 1 ] && tput sgr0 2>&- || :
done
# EOF
which - Obtain a full path to the script if run from $PATH (i.e 'run_blender')
Why do you still include a which when olli@ clarified:​
[…] When you call a shell script via PATH, $0 always includes the directory in which it was found.
cd && pwd - Use temp shell to cd into directory and output current directory name (solves relative paths)
Unnecessary, realpath(1) always returns an absolute pathname. ? FYI, the GNU coreutils version of realpath grealpath(1) lets you retain symbolic links unresolved. This is the only reason I could think of for using the cd/ pwd detour (if it had not been for realpath(1) already resolving symbolic links).​
Bash:
grealpath -L "${0}" # -L prints logical pathname, symlinks remaining unresolved
 
Why do you still include a which when olli@ clarified:​
Because after testing it on a range of different (often non-conforming) platforms, the experience did not align to the clarification. Robustness is key and of course some interpreters strip off the path if it is absolute.

Unnecessary, realpath(1) always returns an absolute pathname.​
Similarly. Robustness. I recall there was an (older) version of busybox who's realpath was not non-conforming on the Windows platform. Cygwin also had issues due to the logical drive mapping. The cd && pwd worked around that.

This script has been evolved over many years to be "robust". Making an assumption that everything is perfectly aligned with POSIX unfortunately undermines that. That said, it is still a "best effort" approach. There are at least two platforms I have used that which fails when a relative path is passed through it. So feel free to cut parts out as needed.
 
Code:
#!/bin/sh

# 0--------------------------------------------------------0
# | validate_ip                                            |
# | resource-efficient sh-script to validate a given v4 ip |
# 0--------------------------------------------------------0

ip=$1
result="."
if [ ${#ip} -lt 7 ]
then
  # -> too short
  result="invalid"
else
  if [ ${#ip} -gt 15 ]
  then
    # -> too long
    result="invalid"
  else
    result="valid"
    if [ $(echo "$ip" | sed 's/[^.]//g' | awk '{ print length }') -ne 3 ]
    then
      # -> no 3 dots
      result="invalid"
    else
      result="valid"
      digit1=$(echo $ip | cut -d '.' -f 1)
      case $digit1 in
        (*[^0-9]*|'')
          # -> not a number
          result="invalid"
        ;;
        (*)
          result="valid"
          if [ $digit1 -lt 0 ] || [ $digit1 -gt 255 ]
          then
            # -> 1st byte invalid (0..255)
            result="invalid"
          else
            digit2=$(echo $ip | cut -d '.' -f 2)
            case $digit2 in
              (*[^0-9]*|'')
                # -> not a number
                result="invalid"
              ;;
              (*)
                result="valid"
                if [ $digit2 -lt 0 ] || [ $digit2 -gt 255 ]
                then
                  # -> 2nd byte invalid (0..255)
                  result="invalid"
                else
                  result="valid"
                  digit3=$(echo $ip | cut -d '.' -f 3)
                  case $digit3 in
                    (*[^0-9]*|'')
                      # -> not a number
                      result="invalid"
                    ;;
                    (*)
                      result="valid"
                      if [ $digit3 -lt 0 ] || [ $digit3 -gt 255 ]
                      then
                        # -> 3rd byte invalid (0..255)
                        result="invalid"
                      else
                        result="valid"
                        digit4=$(echo $ip | cut -d '.' -f 4)
                        case $digit4 in
                          (*[^0-9]*|'')
                            # -> not a number
                            result="invalid"
                          ;;
                          (*)
                            result="valid"
                            if [ $digit4 -lt 0 ] || [ $digit4 -gt 255 ]
                            then
                              # -> 4th byte invalid (0..255)
                              result="invalid"
                            else
                              # -> ip valid
                              result="valid"
                            fi
                          ;;
                        esac
                      fi
                    ;;
                  esac
                fi
              ;;
            esac
          fi
        ;;
      esac
    fi
  fi
fi
echo "$result"
 
Code:
# | resource-efficient sh-script to validate a given v4 ip |
Way too complicated and – in my book – “resource‑efficient” for a shell script (i. e. already consuming quite some resources for an awfully trivial task) implies relying on shell built‑ins as much as possible (not spawning extra processes).​
Bash:
#!/bin/sh
#        name: valid IPv4
# description: validate IPv4 quad‑octet address specification (decimal only)
#  maintainer: Firstname Lastname <mailbox@host> # in multi‐sysadmin environment

# We presume this script never gets `source`d by another script.
# Otherwise overwriting IFS without restoring it is bound to cause trouble.
IFS='.'
# Split the shell script’s first argument on period.
# The empty strings are necessary because the period _separates_ fields.
# Without surrounding empty strings a leading or trailing empty field vanishes.
set -- ''${1:?Usage: $0 ⟨decimal IPv4 address string⟩ # Example: $0 8.8.8.8}''
# Check that there are exactly four octets.
[ $# -eq 4 ] || exit 1
# Check that each octet is a value in the interval 0 – 255.
for octet do
	case ${octet} in
		([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])
			# Intentionally empty.
			;;
		(*)
			exit 1
			;;
	esac
done
# The script returns the last statement’s exit status.
# In this case the `for` statement is otherwise successful.
There is a programming dogma that each sub‑program should fit on one screen, on one printed page. This supposedly improved understanding. Now this implementation certainly fulfills this criterion.

Note that inet_addr(3) accepts octal and hexadecimal numbers, too, and such unusual values as 127.123.45678 (= 2138813038), so in this regard the shell script is incomplete.​
 
Way too complicated and – in my book – “resource‑efficient” for a shell script (i. e. already consuming quite some resources for an awfully trivial task) implies relying on shell built‑ins as much as possible (not spawning extra processes).​
Bash:
#!/bin/sh
#        name: valid IPv4
# description: validate IPv4 quad‑octet address specification (decimal only)
#  maintainer: Firstname Lastname <mailbox@host> # in multi‐sysadmin environment

# We presume this script never gets `source`d by another script.
# Otherwise overwriting IFS without restoring it is bound to cause trouble.
IFS='.'
# Split the shell script’s first argument on period.
set -- $1
# Check that there are exactly four octets.
[ $# -eq 4 ] || exit 1
# Check that each octet is a value in the interval 0 – 255.
for octet do
    case ${octet} in
        ([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])
            # Intentionally empty.
            ;;
        (*)
            exit 1
            ;;
    esac
done
# The script returns the last statement’s exit status.
# In this case the `for` statement is otherwise successful.
There is a programming dogma that each sub‑program should fit on one screen, on one printed page. This supposedly improved understanding. Now this implementation certainly fulfills this criterion.

Note that inet_addr(3) accepts octal and hexadecimal numbers, too, and such unusual values as 127.123.45678 (= 2138813038), so in this regard the shell script is incomplete.​
The trick is to stop processing at the first fault, and have the possible dropouts ordered in likeliness.
 
As I wrote, this script only takes decimal specifications. The leading zero is – by the standardized inet_addr(3) function – interpreted as an octal base indicator. That means 010.000.001.001 = 8.0.1.1 = 134217985.​
Yes, but people are largely ignorant of the subtleties of IP(v4) address formats. But, many equipment vendors will flush out each octet to a three digit value "to look nice" or to arrange for addresses to nicely align in columnar outputs. Folks copying a script would be likely be ignorant of this unless explicitly stated.

[IIRC, my NCDs were this way. And I think at least one of my QNAPs has a "two button" front panel feature to set IP address that relies on three digit fields -- so you don't have to count up from "0", one at a time.]

Sadly, there is no way to alert a user providing input in that form of the potential for misinterpretation -- unless you see an invalid octal character prefaced with a leading zero, etc.

Safer to insist on an absence of leading zeroes and throw an error if any are detected (another case in your script). I.e., error handling (as always) often leads to code bloat.
 
Code:
#!/usr/local/bin/bash
# 0----------------------------------------------------------------------------0
# | play_doom.sh                                                               |
# | Trying to be non-interactive:                                              |
# | Download Doom 1 shareware version, auto-configure                          |
# | and play in Dosbox in attempted fullscreen                                 |
# | - using www.dosgamesarchive.com to download the game archive.              |
# | - programs emulators/dosbox and x11/xdotool must be installed              |
# | - using /tmp directory, usually RAM-based so nothing is stored permanently |
# 0----------------------------------------------------------------------------0
cd /tmp
mkdir doom
cd doom
fetch https://www.dosgamesarchive.com/file.php?id=987
# 0------------------0
# | this is a zip... |
# 0------------------0
mv "file.php?id=987" doom.zip
unzip doom.zip
# 0-----------------------0
# | trying get fullscreen |
# 0-----------------------0
echo "[render]" > /tmp/doom/dosbox.conf
echo "aspect=true" >> /tmp/doom/dosbox.conf
echo "scaler=normal3x" >> /tmp/doom/dosbox.conf
# 0---------------------------------------------------------------------0
# | Automated DOS installation procedure                                |
# | (Use xdotool to push timed keyboard actions ahead of the processes) |
# 0---------------------------------------------------------------------0
(
  sleep 1
  xdotool key c
  sleep .1
  xdotool key Enter
  sleep .1
  xdotool key Y
) & dosbox -conf /tmp/doom/dosbox.conf -c "mount c ." -c "c:" -c "deice.exe" -c "exit"
# 0----------------------------------------------------------0
# | Extracting is too slow in Dosbox, so we do that outside. |
# 0----------------------------------------------------------0
cd /tmp/doom/DOOMS
unzip DOOMS_19.EXE
(
  sleep 1
  xdotool key Escape
  sleep .5
  xdotool key Escape
  sleep .5
  xdotool key Escape
  sleep .5
  xdotool key Enter
) & dosbox -fullscreen -conf /tmp/doom/dosbox.conf -c "mount c ." -c "c:" -c "setup"
 
Hi people!

I'm coming here after this thread, where I'm asking about utility that can unambiguously determine the source code directory for executable in FreeBSD.

Well, as we see, there is no such utility quite yet. So I decided to make my own one.

So, here's the script that I've written (all credits go to covacat, on top of whose script I've actually built mine. You can find his original script in the abovementioned thread):

sh:
#!/bin/sh

# src -- locate source code for executable in FreeBSD.
#
# It determines the source code depending on information
# that can be obtained from Makefiles under /usr/src.
# Operations with Makefiles require certain time, that's
# why it does it once and stores the information in the index
# file (under /usr/local/share/src by default), that is later
# issued for actual fast searching.


TOP="/usr/src"
INDEX_DIR="/usr/local/share/src"
INDEX_NAME="base.index"
INDEX="$INDEX_DIR/$INDEX_NAME"

# The program supports two forms of invocation:
#  `-u [-v]': creating an index file that we will make use of when
#             actually locating the sources.
#             `-v' activates verbose mode - added lines will be printed.
#  `[-a] file ...': find sources for specified files.
#                   With `-a' all the found paths are printed.
usage() {
    echo "usage: src -u [-v]
       src [-a] file ..."
    exit 1
}

if [ "$#" = "0" ];then
    usage
fi

# Check if we have a source tree.
if [ ! -d "$TOP" ];then
    echo "[src]: source code directory $TOP doesn't exist." 1>&2
    exit 1
fi

# The program is invoked in "update" mode, which creates an index file.
# The index file consists of lines:
#   `<executable path and its hard links, if any> . <source paths>'.
# Example:
#   `/bin/ed /bin/ed /bin/red . /usr/src/bin/ed'.
if [ "$1" = "-u" ];then
    # If in the verbose mode, the actual lines that go to the index
    # file will be directed to stdout as well.
    if [ "$2" = "-v" ];then
        verbose="1"
    fi
   
    # Delete previously created index file if it exists.
    rm -f "$INDEX" 2>/dev/null
    # Create a path for index if it doesn't exist.
    mkdir -p "$INDEX_DIR" 2>/dev/null
    if [ $? -ne 0 ];then
        echo "[src]: can not create directory for index file $INDEX_DIR." 1>&2
        exit 1
    fi
   
    # Since index file will be situated under /usr/local/share, only
    # root user will be able to modify the file. We want to check if
    # we have appropriate permissions for this before we start.
    if ! touch "$INDEX" 2>/dev/null;then
        echo "[src]: can not create an index file at $INDEX." 1>&2
        exit 1
    fi
   
    # Collect all the Makefiles we possibly will be interested in.
    # Also avoiding lots of Makefiles that are for tests.
    for mf in $(find ${TOP}/bin/ ${TOP}/sbin/ \
                     ${TOP}/usr.bin/ ${TOP}/usr.sbin/ \
                     ${TOP}/libexec/ \
                     ${TOP}/secure/usr.bin/ ${TOP}/secure/usr.sbin/ \
                     ${TOP}/cddl/ ${TOP}/kerberos5/ \
                -type f \
                -name Makefile \
                -not -path "*\/*tests\/*" \
                -maxdepth 4)
    do
        mfd=$(dirname $mf)
       
        # Now we want to filter the Makefiles we don't need.
        # We're only looking for those ones that are for producing
        # binaries (PROG or PROG_CXX) or scripts (man(1), for example,
        # is actually a shell script, but we want to be able to search
        # for its source too).
        if grep -Eq "(PROG(_CXX)|SCRIPTS)?\??( |    )*=" $mf ;then
            # Here we're obtaining paths that this Makefile searches through.
            # It's stored in `.PATH' variable.
            sea_paths=$(make -C "$mfd" \
                            -V '${.PATH}' \
                            -f $mf \
                       2>/dev/null \
                       |sed "s/\.//")
           
            # If no such paths, then just skip this Makefile.
            if [ "$sea_paths" = " ." ];then
                continue
            fi
           
            seas=" ."
            # Now we try to filter out all the search paths
            # that do not actually contain the source code.
            # If the path does not have source code files for
            # C, C++ programs or shell scripts, or there are
            # no executable files (that usually go without extensions),
            # then it's most likely not a source code directory.
            for sea in $sea_paths;do
                prog_files=$(find "$sea" -type f \
                                \( -perm -111 \
                                -or -name "*\.c" \
                                -or -name "*\.cpp" \
                                -or -name "*\.cc" \
                                -or -name "*\.sh" \
                                -or -name "*\.y" \) \
                                -maxdepth 1)
                if [ -n "$prog_files" ];then
                    seas="$seas $sea"
                fi
            done
           
            # Obtain binary destination path and its hard links.
            bin_and_links=$(make -C "$mfd" \
                                 -V '${BINDIR}/${PROGNAME} ${LINKS}' \
                                 -f $mf \
                            2>/dev/null
            )
           
            # We have included scripts in our search along with binaries,
            # but a lot of Makefiles produce scripts that are used for
            # building, I guess, and they are not system-wide.
            # We can tell it's one of such scripts if its search
            # paths look like that:
            if [ "$bin_and_links" = "/ " ] \
               || [ "$bin_and_links" = "/bin/ " ] \
               || [ "$bin_and_links" = "/sbin/ " ] \
               || [ "$bin_and_links" = "/usr/bin/ " ] \
               || [ "$bin_and_links" = "/usr/sbin/ " ];then
                continue
            fi
           
            # Build up an index line entry.
            index_line=$(echo "$bin_and_links$seas" |sed 's/  / /g')
            if [ "$verbose" = "1" ];
            then echo "$index_line" |tee -a "$INDEX"
            else echo "$index_line" >>"$INDEX"
            fi
        fi
    done
   
    exit 0
fi

# If we've reached this place, this means we're in the
# second invocation form (searching sources).

# First, check if we have an index file.
if [ ! -f "$INDEX" ];then
    echo "[src]: index file $INDEX not found." 1>&2
    exit 1
fi

# In "print all" mode all the paths, that were found
# (usually more than one path is found, when the program is
# built of several modules or it also can be that the program
# includes a source code of some external program).
if [ "$1" = "-a" ];then
    all_mode="1"
    shift
   
    if [ "$#" = "0" ];then
        usage
    fi
fi

# Search paths for specified programs one-by-one.
tgts="$@"
for tgt in $tgts;do
    tgt_path=$(which $tgt 2>/dev/null)
   
    if [ "$?" != "0" ];then
        echo "[src]: $tgt is not found." 1>&2
        continue
    fi
   
    # Resolve symlinks (like tar(1) is a symlink to /usr/bin/bsdtar).
    # And also escape characters like "[" and "." for executables
    # like [(1) (which is also test(1)) (otherwise, they will be
    # treated as part of regex sytax by grep(1)).
    tgt_path=$(realpath "$tgt_path" \
               |sed -e 's/\[/\\\[/g' \
                    -e 's/\./\\\./g')
   
    # Search the target path in the index file.
    # Looking only into the first part of line, where binaries and links.
    srcs=$(grep "$tgt_path .*\. " "$INDEX")
   
    if [ -z "$srcs" ];then
        echo "[src]: no sources found for $tgt." 1>&2
        continue
    fi
   
    srcs=$(echo "$srcs" |sed 's/.* \. //')
    # Print only first path or all of them if appropriate flag is set.
    for src in $srcs;do
        echo "$src"
       
        if [ "$all_mode" != "1" ];then
            break
        fi
    done
done

First it needs to create an index file (according to hier(7) I decided that /usr/local/share would be the right place) with (as root): src -u (also you can provide -v flag to see the lines written to the file). It's needed because walking through /usr/src and issuing lots of Makefiles takes long time. As a result, index file takes 59K on my machine.

And when it's done you can find sources with src <program path or name> ... (you can also provide -a flag to print all the paths were found).

I tried to make it cover all the (edge)cases, because it appeared that there are some tricky ones. Let me show you some examples:

1) src sha1 -> /usr/src/sbin/md5.
/sbin/sha1 is a hardlink to /sbin/md5.

2) src strip -> /usr/src/contrib/elftoolchain/elfcopy.
Also hard link case, but source directory is not that obvious.

3) src tar -> /usr/src/contrib/libarchive/tar.
/usr/bin/tar is a symlink to /usr/bin/bsdtar.

4) src man -> /usr/src/usr.bin/man.
I didn't know, but /usr/bin/man is a shell script. It also works.

However, there are few cases that don't work or perhaps work not exactly the way we want them to:

1) src make -> /usr/src/contrib/bmake/filemon
The scripts looks into .PATH make(1) variable to find the source path. But some sources include external (or additional) modules *before* the main module. As you can see, this is one of these cases. But, well, from the path it returned it's still possible to figure that actual source directory is one level up.

2) src wpa_cli -> can't find sources.
I sorta found what's the problem. There is a line from /usr/src/usr.sbin/wpa/wpa_cli/Makefile:
Makefile:
.PATH.c:${WPA_SUPPLICANT_DISTDIR}
Yeah, it uses .PATH.c instead of .PATH. But when I try to execute make -V '${.PATH.c}' on this file, it doesn't give anything (but it works with .PATH, which doesn't include the path we need). Here I want to ask you: maybe you know why this happens / any ideas on how to handle this case?

Well, it seems to be all. You can also look into source code itself - I tried to leave explanatory comments.

Thank you,
Artem
 
Back
Top