Tag Archives: shell

Split Audio Files into Parts

I recently got in the need of splitting quite large amount of audio files into smaller equal parts. The first thought that came to my mind was – probably thousand or more people had similar problem in the past so its already solved – so I went directly to the web search engine.

The found solutions seem not that great or work partially only … or not work like I expected them to work. After looking at one of the possible solutions in a bash(1) script I started to modify it … but it turned out that writing my own solution was faster and easier … and simpler.

Today I will share with you my solution to automatically split audio files into small equal parts.

Existing Solutions

In my search for existing solutions I indeed found some tools that will allow me to achieve what I need. I will not try to talk them one after another.

mp3splt

The first one I found was the audio/mp3splt port (and package) available on FreeBSD. So I installed it with typical pkg(8) command as shown below.

# pkg install mp3splt

It installed properly … but returned Segmentation Fault instead of actually working. I even submitted a PR for that in the FreeBSD Bugzilla – 264866 – but no update till now.

Thus I removed that package and went to search for something that works.

Brasero

Someone on some forum suggested using CD/DVD burning software – Brasero – because one of its features is audio splitting – so I installed the sysutils/brasero package now.

# pkg install brasero

It turns out that it really works. Some screenshots below.

brasero.1

brasero.2

… but that did not satisfied my because I wanted an automated/unattended solution instead of ‘clicking’ each file separately to split them. I also did not liked the fact that I needed to specify time in seconds.

mp3split

Do not confuse with mentioned earlier mp3splt command. The mp3split is a unattended one created in a bash(1) script – https://diegosanchezp.github.io/blog/mp3split/ – available and described here. One of its downsides (for me) was that it needed additional external ‘list’ file with times and titles for the parts.

I did not wanted to write this each time so I generated a long enough list file that will cover any possible file no matter the length with the following loop.

% seq 0 10 10000 \
    | while read MIN
      do
        seq 0 10 50 \
          | while read SEC
            do
              echo ${MIN}:${SEC}
            done
      done > list.txt

% head list.txt
0:0
0:10
0:20
0:30
0:40
0:50
10:0
10:10
10:20
10:30

I needed to split these audio files every 10 minutes. I redirected that output into the list.txt file. I then fetched and made executable the mentioned mp3split script.

% fetch https://raw.githubusercontent.com/diegosanchezp/mp3split/master/mp3split.sh

% chmod +x mp3split.sh

% ./mp3split.sh --help
zsh: ./mp3split.sh: bad interpreter: /bin/bash: no such file or directory

% head -1 ./mp3split.sh
#!/bin/bash

So now we will have to remove linuxisms from the script. Lets hope its only the interpreter part.

% head -1 ./mp3split.sh
#! /usr/bin/env bash

% ./mp3split.sh --help
./mp3split.sh: illegal option -- -
Invalid option: -
Usage:
  mp3split [OPTIONS] inputaudio tracklist
Options:
  -s: do a simulation without writing anything to disk
  -h: print this help
  -e extension: set output extension, if extension is equal to "" keep extension of input file
  The script will output all the splitted files in the
  current/working directory.


Better. Lets try to use it.

% ./mp3split.sh LARGE-AUDIO-FILE.mp3 list.txt

=== Begin to create mp3 split files ===
0:0.mp3: Protocol not found
Processed 0:0 to 0:10; 0:0.mp3
0:10.mp3: Protocol not found
Processed 0:10 to 0:20; 0:10.mp3
0:20.mp3: Protocol not found
Processed 0:20 to 0:30; 0:20.mp3
0:30.mp3: Protocol not found
Processed 0:30 to 0:40; 0:30.mp3
0:40.mp3: Protocol not found
Processed 0:40 to 0:50; 0:40.mp3
0:50.mp3: Protocol not found
Processed 0:50 to 10:0; 0:50.mp3
10:0.mp3: Protocol not found
Processed 10:0 to 10:10; 10:0.mp3
10:10.mp3: Protocol not found
Processed 10:10 to 10:20; 10:10.mp3
10:20.mp3: Protocol not found
Processed 10:20 to 10:30; 10:20.mp3
10:30.mp3: Protocol not found
Processed 10:30 to 10:40; 10:30.mp3
10:40.mp3: Protocol not found
Processed 10:40 to 10:50; 10:40.mp3
10:50.mp3: Protocol not found
Processed 10:50 to 20:0; 10:50.mp3
20:0.mp3: Protocol not found
Processed 20:0 to 20:10; 20:0.mp3
20:10.mp3: Protocol not found
Processed 20:10 to 20:20; 20:10.mp3
20:20.mp3: Protocol not found
Processed 20:20 to 20:30; 20:20.mp3
^C

Some strange error message Protocol not found … after small investigation it turns out that two characters fix for the ffmpeg(1) command will do. The diff(1) is available below.

% diff -u mp3split.sh mp3split.sh.FIXED.sh
--- mp3split.sh 2022-06-25 22:34:25.499718000 +0200
+++ mp3split.sh.FIXED.sh        2022-06-25 22:37:45.580845000 +0200
@@ -25,7 +25,7 @@
   outfile="$tracktitle.$ext"

   # Begin splitting files with ffmpeg
-  [ ! "$simulate" = true ] && ffmpeg -nostdin -y -loglevel error -i "$inputaudio" -ss "$start" -to "$end" -acodec copy "$outfile"
+  [ ! "$simulate" = true ] && ffmpeg -nostdin -y -loglevel error -i "$inputaudio" -ss "$start" -to "$end" -acodec copy ./"$outfile"

   echo "Processed $start to $end; $outfile"
 }

Now lets try to use the fixed version.

% ./mp3split.sh.FIXED.sh LARGE-AUDIO-FILE.mp3 list.txt

=== Begin to create mp3 split files ===
Processed 0:0 to 0:10; 0:0.mp3
Processed 0:10 to 0:20; 0:10.mp3
Processed 0:20 to 0:30; 0:20.mp3
Processed 0:30 to 0:40; 0:30.mp3
Processed 0:40 to 0:50; 0:40.mp3
Processed 0:50 to 10:0; 0:50.mp3
Processed 10:0 to 10:10; 10:0.mp3
Processed 10:10 to 10:20; 10:10.mp3
Processed 10:20 to 10:30; 10:20.mp3
Processed 10:30 to 10:40; 10:30.mp3
Processed 10:40 to 10:50; 10:40.mp3
Processed 10:50 to 20:0; 10:50.mp3
Processed 20:0 to 20:10; 20:0.mp3
Processed 20:10 to 20:20; 20:10.mp3
Processed 20:20 to 20:30; 20:20.mp3
Processed 20:30 to 20:40; 20:30.mp3
Processed 20:40 to 20:50; 20:40.mp3
Processed 20:50 to 30:0; 20:50.mp3
Processed 30:0 to 30:10; 30:0.mp3
Processed 30:10 to 30:20; 30:10.mp3
Processed 30:20 to 30:30; 30:20.mp3
Processed 30:30 to 30:40; 30:30.mp3
Processed 30:40 to 30:50; 30:40.mp3
Processed 30:50 to 40:0; 30:50.mp3
Processed 40:0 to 40:10; 40:0.mp3
Processed 40:10 to 40:20; 40:10.mp3
Processed 40:20 to 40:30; 40:20.mp3
Processed 40:30 to 40:40; 40:30.mp3
Processed 40:40 to 40:50; 40:40.mp3
Processed 40:50 to 50:0; 40:50.mp3
Processed 50:0 to 50:10; 50:0.mp3
Processed 50:10 to 50:20; 50:10.mp3
Processed 50:20 to 50:30; 50:20.mp3
Processed 50:30 to 50:40; 50:30.mp3
Processed 50:40 to 50:50; 50:40.mp3
Invalid duration specification for to: 60:0
Processed 50:50 to 60:0; 50:50.mp3
Invalid duration specification for ss: 60:0
Processed 60:0 to 60:10; 60:0.mp3
Invalid duration specification for ss: 60:10
Processed 60:10 to 60:20; 60:10.mp3
Invalid duration specification for ss: 60:20
Processed 60:20 to 60:30; 60:20.mp3
Invalid duration specification for ss: 60:30
Processed 60:30 to 60:40; 60:30.mp3
Invalid duration specification for ss: 60:40
Processed 60:40 to 60:50; 60:40.mp3
Invalid duration specification for ss: 60:50
Processed 60:50 to 70:0; 60:50.mp3
Invalid duration specification for ss: 70:0
Processed 70:0 to 70:10; 70:0.mp3
Invalid duration specification for ss: 70:10
Processed 70:10 to 70:20; 70:10.mp3
^C

Great … so after the file ended it will still try EVERY goddamn position from the list.txt file. It was also not able to reach the final ‘ending’ part without ‘visiting’ each time from the list.txt file. Enough is enough. I tried.

Custom Script Solution

After trying to modify the mp3split script even more I came to the conclusion that it will take less time to write my own solution from scratch … and this is exactly what I did. I wrote the audio-split.sh in POSIX /bin/sh interpreter for portability. After an hour later 50 lines of code did exactly what I needed – not counting the __usage() function for help information.

code.fixed

Here is the __usage() contents by the way.

help

The idea/needs were:

  • split large file automatically/unattended into equal parts
  • create new dir in which these parts are created
  • new dir must have same name as specified file (without extension)
  • each part will get a ' - xxx' suffix (like ' - 001' for first part) with original extension

… and they were met.

Here is the output of running audio-split.sh command.

% ffmpeg -i LARGE-AUDIO-FILE.mp3 2>&1 | grep Duration
  Duration: 00:44:55.99, start: 0.025057, bitrate: 171 kb/s

% audio-split.sh 10 LARGE-AUDIO-FILE.mp3
LARGE-AUDIO-FILE/LARGE-AUDIO-FILE - 001.mp3
LARGE-AUDIO-FILE/LARGE-AUDIO-FILE - 002.mp3
LARGE-AUDIO-FILE/LARGE-AUDIO-FILE - 003.mp3
LARGE-AUDIO-FILE/LARGE-AUDIO-FILE - 004.mp3
LARGE-AUDIO-FILE/LARGE-AUDIO-FILE - 005.mp3

% du -sm LARGE-AUDIO-FILE.mp3
56      LARGE-AUDIO-FILE.mp3

% du -smc LARGE-AUDIO-FILE/*
13      LARGE-AUDIO-FILE/LARGE-AUDIO-FILE - 001.mp3
13      LARGE-AUDIO-FILE/LARGE-AUDIO-FILE - 002.mp3
13      LARGE-AUDIO-FILE/LARGE-AUDIO-FILE - 003.mp3
13      LARGE-AUDIO-FILE/LARGE-AUDIO-FILE - 004.mp3
7       LARGE-AUDIO-FILE/LARGE-AUDIO-FILE - 005.mp3
56      total

The total size is the same (or similar in larger files). After listening to the parts I came to the conclusion that it works properly. The audio file is about 45 minutes long and the script created 4 10 minutes long files and 1 that is less then 5 minutes. Not sure if you also have such needs but if yes then you may now use another solution – audio-split.sh – for it πŸ™‚

EOF

Ghost in the Shell – Part 7 – ZSH Setup

Today I would like to share with you my simple yet useful zsh(1) shell config that I use daily.

You may want to check other articles in the Ghost in the Shell series on the Ghost in the Shell – Global Page where you will find links to all episodes of the series along with table of contents for each episode’s contents.

I have been various UNIX and Linux systems since almost two decades. Through that time I was always looking for the best interactive shell out there. Obviously I have started with the bash(1) on Linux and it generally worked but bash(1) also did not impressed me at all. Just a shell. I have similar experiences with the ksh(1) shell. Today even the plain POSIX /bin/sh shell on FreeBSD has basic completion similar in many ways to what bash(1) or ksh(1) allows. The bash(1) shell gets little better when you install the bash-completion companion but its very limited still.

When I moved to FreeBSD I got to know its default tcsh(1)/csh(1) shell … which is PITA to use and scripting. Its pointless to learn limited CSH shell syntax in 2021. Omit it at any costs. After I settled a little in the FreeBSD wonderland I started to try other shells such as zsh(1) or fish(1) shells. I really liked fish(1) shell preconfigured setup and its defaults because it required literally zero effort to use it at its peak possibilities … but when I tried one of my typical use cases which is some commands | while read I; other command "${I}"; done it came to me that fish(1) is very limited shell and even does not support critical POSIX /bin/sh syntax! What a disappointment to say the last.

I abandoned the fish(1) shell and went to the zsh(1) which by default does not do more then a bash(1) shell and needs well thought configuration to be useful and powerful. After checking some guides and howtos about zsh(1) shell I started to create my own config and this was the interactive environment I was looking for. Of course I had several newbie problems or things that did not worked well for me like for example automatic completion of user home directories or UPPERCASE to lowercase automatic translation but after digging more into the zsh(1) config and man pages I finally settled with sensible and reasonable zsh(1) shell config.

I also tried various ready to use zsh(1) preconfigurations such as PowreLevel10k or Oh-My-Zsh but none of them really satisfied my while being kinda ‘blackbox’ with features that I do not really need. I really like to use things that I understand under the hood so I stayed with my quite simple yet fast loading config.

Why ZSH Shell Anyway?

Besides The Usual Suspects (really great movie by the way) like recursive search with [CTRL]+[R] for forward search and [CTRL]+[SHIFT]+[R] for reverse search – argument completion for most commands like shown below.

% tar -[TAB]
A  -- append to an archive
c  -- create a new archive
f  -- specify archive file or device
t  -- list archive contents
u  -- update archive
v  -- verbose output
x  -- extract files from an archive

The graphical example of that can be shown here. Of course I am not able to show [TAB] key there as I am able to add in the ‘text’ examples.

zsh-gstat

Interactive argument completion like showing the list of processes you can kill(1) by pressing the [TAB] key while being at kill(1) or killall(1) commands.

% kill -9 [TAB]
 9289  4  Ss+  0:00.62 -zsh (zsh)
16994  2  Is   0:00.28 -zsh (zsh)
17860  1  Is+  0:00.17 -zsh (zsh)
23797  3  Is+  0:00.23 -zsh (zsh)
30335  4  S+   0:00.01 -zsh (zsh)
32381  4  R+   0:00.00 ps
44994  0  Is+  0:00.50 -zsh (zsh)
59828  2  I+   0:00.02 /bin/sh /usr/bin/man zsh
65435  2  I+   0:00.05 less

Similar with the pkill(1) command when trying to autocomplete with ‘h‘ letter. The filter adds all running processes that have ‘h‘ letter in them – not only processes that start with the ‘h‘ letter.

zsh-pkill

There are also other more sophisticated completions like completioning the file name but not from the front but by the part of it … or by extension. Take a look at these two examples below. This is out directory listing that we will be using as an example here.

% exa -l
drwxr-xr-x - vermaden 2021-09-18 21:47 and a really PITA dir with spaces
.rw-r--r-- 0 vermaden 2021-09-18 21:54 huge.iso
.rw-r--r-- 0 vermaden 2021-09-18 21:46 really.async.example.txt
.rw-r--r-- 0 vermaden 2021-09-18 21:47 some-plain-file.txt

Three files and one directory with spaces in its name.

To autocomplete any of them with bash(1)you would have to start typing the file or dir name from the beginning. The fish(1) shell is on par with zsh(1) here as it would also support the thing that I want to show you.

First things first – the cd(1) command to change current working directory. Because there is only ONE directory there both zsh(1) and fish(1) shells would properly autocomplete the only once and a really PITA dir with spaces dir for the cd(1) command like shown below.

% exa -l
drwxr-xr-x - vermaden 2021-09-18 21:47 and a really PITA dir with spaces
.rw-r--r-- 0 vermaden 2021-09-18 21:54 huge.iso
.rw-r--r-- 0 vermaden 2021-09-18 21:46 really.async.example.txt
.rw-r--r-- 0 vermaden 2021-09-18 21:47 some-plain-file.txt

% cd [TAB]

// after pressing [TAB] once becomes this

% cd and\ a\ really\ PITA\ dir\ with\ spaces

The bash(1) (and csh(1)/tcsh(1) for the record) would obviously need to start from the first letter of any of those dir or files trying the really stupid completion method.

Now the second part about completion of files extensions or names in the middle of dirs or files. Both zsh(1) and fish(1) shells support that. Examples below.

% exa -l
drwxr-xr-x - vermaden 2021-09-18 21:47 and a really PITA dir with spaces
.rw-r--r-- 0 vermaden 2021-09-18 21:54 huge.iso
.rw-r--r-- 0 vermaden 2021-09-18 21:46 really.async.example.txt
.rw-r--r-- 0 vermaden 2021-09-18 21:47 some-plain-file.txt

% cat txt[TAB]

// NOW zsh(1) will show all files that have 'txt' string in it

% cat le.txt[TAB]

// AFTER SECOND [TAB] HIT IT WILL LIST THEM WITH MENU FEATURE (MORE ON THAT IN A MOMENT)

% cat really.async.example.txt[TAB]
really.async.example.txt  some-plain-file.txt

// NOW FIRST FILE WITH 'txt' IS USED - HIT [TAB] AGAIN TO SWITCH TO NEXT ONE

% cat some-plain-file.txt[TAB]
really.async.example.txt  some-plain-file.txt

// YOU CAN ALSO USE ARROW KEYS TO SELECT BETWEEN THEM - CHECK SCREENSHOT BELOW

Example of menu completion feature below.

zsh-menu-completion

Time to stop showing off and start to provide some useful content.

System Config

There are lots of guides and ideologies about how you spread your zsh(1) configuration between system wide config file and user customized ones like these:

  • /etc/zshenv
  • /etc/zprofil
  • /etc/zshrc
  • /etc/zlogin
  • /etc/zlogout
  • ~/.zshenv
  • ~/.zprofile
  • ~/.zshrc
  • ~/.zlogin
  • ~/.zlogout

My take? Lets not make some big issue about that. I really like simple sensible setups and I use zsh(1) as interactive shell so ‘system wide’ configuration is not crucial here. To make things as simple as possible I only use two of all of the above. The /usr/local/etc/zshrc for the ‘system wide’ part and ~/.zshrc for my ‘user’ part. Thats it. I have been doing that since more then a decade and it worked for me like a charm but as in every case your millage may vary here.

As there are too many Linuxisms out there assuming that you are using Ubuntu Linux or that bash(1) shell is always available as /bin/sh binary after 16 years of me using FreeBSD UNIX there for sure will several BSDisms but at least they are harmless and documented πŸ™‚

The /usr/local/etc/zshrc (or should I say /etc/zshrc on Linux and other then FreeBSD UNIX systems) is as follows.

# BASICS
  umask 022
  export PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin
  export EDITOR=vi
  export PAGER=less

# USE ftp(1) PASSIVE MODE
  export FTP_PASSIVE_MODE=1

# DISABLE less(1) HISTORY
  export LESSHISTSIZE=0

# IMITATE sockstat(1) ON LINUX
  case $( uname ) in
    (Linux) alias sockstat="netstat -lnptu --protocol=inet,unix" ;;
  esac

# ZSH HISTORY
  export HISTSIZE=655360
  export HISTFILE="${HOME}/.zhistory"
  export SAVEHIST=${HISTSIZE}

# ZSH HISTORY SEARCH
  bindkey -M vicmd '/' history-incremental-pattern-search-backward
  bindkey -M vicmd '?' history-incremental-pattern-search-forward

# ZSH HISTORY SEARCH FOR vi(1) INSERT MODE
  bindkey -M viins '^R' history-incremental-pattern-search-backward
  bindkey -M viins '^F' history-incremental-pattern-search-forward

# ZSH HISTORY MAPPINGS
  bindkey '^[[A' up-line-or-search
  bindkey '^[[B' down-line-or-search
  bindkey "^R" history-incremental-search-backward

# ZSH USE SHIFT-TAB FOR REVERSE COMPLETION
  bindkey '^[[Z' reverse-menu-complete

# ZSH LAST ARG FROM EARLIER COMMAND WITH [ALT]-[.]
  bindkey '\e.' insert-last-word

# ZSH BEGIN/END OF LINE
  bindkey "^A" beginning-of-line
  bindkey "^E" end-of-line

# KEY BINDINGS
  case "${TERM}" in
    (cons25*|linux)
      # PLAIN BSD/LINUX CONSOLE
      bindkey '\e[H'    beginning-of-line   # HOME
      bindkey '\e[F'    end-of-line         # END
      bindkey '\e[5~'   delete-char         # DELETE
      bindkey '[D'      emacs-backward-word # ESC+LEFT
      bindkey '[C'      emacs-forward-word  # ESC+RIGHT
      ;;
    (*rxvt*)
      # RXVT DERIVATIVES
      bindkey '\e[3~'   delete-char         # DELETE
      bindkey '\eOc'    forward-word        # CTRL+RIGHT
      bindkey '\eOd'    backward-word       # CTRL+LEFT
      # RXVT WORKAROUND FOR screen(1) UNDER urxvt(1)
      bindkey '\e[7~'   beginning-of-line   # HOME
      bindkey '\e[8~'   end-of-line         # END
      bindkey '^[[1~'   beginning-of-line   # HOME
      bindkey '^[[4~'   end-of-line         # END
      ;;
    (*xterm*)
      # XTERM DERIVATIVES
      bindkey '\e[H'    beginning-of-line   # HOME
      bindkey '\e[F'    end-of-line         # END
      bindkey '\e[3~'   delete-char         # DELETE
      bindkey '\e[1;5C' forward-word        # CTRL+RIGHT
      bindkey '\e[1;5D' backward-word       # CTRL+LEFT
      # XTERM WORKAROUND FOR screen(1) UNDER xterm(1)
      bindkey '\e[1~'   beginning-of-line   # HOME
      bindkey '\e[4~'   end-of-line         # END
      ;;
    (screen)
      # GNU SCREEN
      bindkey '^[[1~'   beginning-of-line   # HOME
      bindkey '^[[4~'   end-of-line         # END
      bindkey '\e[3~'   delete-char         # DELETE
      bindkey '\eOc'    forward-word        # CTRL+RIGHT
      bindkey '\eOd'    backward-word       # CTRL+LEFT
      bindkey '^[[1;5C' forward-word        # CTRL+RIGHT
      bindkey '^[[1;5D' backward-word       # CTRL+LEFT
      ;;
  esac

# ZSH COMPLETION CASE (IN)SENSITIVE
# zstyle ':completion:*' matcher-list 'm:{a-zA-Z}={A-Za-z}'

# ZSH DISABLE USER COMPLETION FOR THESE NAMES
  zstyle ':completion:*:*:*:users' ignored-patterns \
    dladm dbus distcache dovecot list ftp games gdm gkrellmd gopher gnats \
    adm amanda apache avahi backup beaglidx bin cacti canna clamav daemon \
    sshd sync sys syslog uucp vcsa smmsp svctag upnp unknown webservd xfs \
    listen mdns fax mailman mailnull mldonkey mysql man messagebus netadm \
    hacluster haldaemon halt hsqldb mail junkbust ldap lp irc xvm libuuid \
    nscd ntp nut nx ident openldap operator pcap pkg5srv postfix postgres \
    netcfg nagios noaccess nobody4 openvpn named netdump nfsnobody nobody \
    proxy privoxy pulse pvm quagga radvd rpc rpcuser shutdown statd squid \
    www-data news nuucp zfssnap rpm '_*'

# ZSH COMPLETION OPSTIONS
  zstyle ':completion:*' completer _expand _complete _correct _approximate _history
  zstyle ':completion:*' matcher-list '' '' 'l:|=* r:|=*' 'l:|=* r:|=*'
  zstyle ':completion:*' list-colors ''
  zstyle ':completion:*' users root
  zstyle ':completion:*' menu select
  zstyle :compinstall filename '~/.zshrc'
  autoload -Uz compinit
  autoload -U colors && colors
  compinit

# ZSH OTHER FEATURES
  unsetopt beep
  setopt no_beep
  setopt nohashdirs
  setopt extended_glob
  setopt auto_cd
  setopt auto_menu
  setopt list_rows_first
  setopt multios
  setopt hist_ignore_all_dups
  setopt append_history
  setopt inc_append_history
  setopt hist_reduce_blanks
  setopt always_to_end
  setopt no_hup
  setopt complete_in_word
  limit coredumpsize 0

# ZSH zshcontrib(1) zmv
  autoload zmv
  alias zmv_to_lower='zmv      "*" "\${(L)f}"'
  alias zmv_to_upper='zmv      "*" "\${(U)f}"'
  alias zmv_to_capital='zmv    "*" "\${(C)f}"'
  alias zmv_to_hypen='zmv      "*" "\$f:gs/ /-/"'
  alias zmv_to_underscore='zmv "*" "\$f:gs/ /_/"'

# COLOR grep(1)
  export GREP_COLOR='1;32'
  export GREP_COLORS='1;32'
  export GREP_OPTIONS='--color=auto'
  alias grep='grep --color'
  alias egrep='egrep --color'

# FreeBSD ifconfig(8) CIDR NOTATION
  export IFCONFIG_FORMAT=inet:cidr

# SET ls(1) COLORS
  export LSCOLORS='exExcxdxcxexhxhxhxbxhx'
  export LS_COLORS='no=00:fi=00:di=00;34:ln=00;36:pi=40;33:so=00;35:bd=40;33;01:cd=40;33;01:or=01;05;37;41:mi=01;05;37;41:ex=00;32'

# DISABLE XON/XOFF FLOW CONTROL (^S/^Q)
  stty -ixon

# COLOR LIST
# 30 - black     # 34 - blue
# 31 - red       # 35 - magenta
# 32 - green     # 36 - cyan
# 33 - yellow    # 37 - white

# COLOR PROMPT
  cSRV="%F{magenta}"
  case $( whoami ) in
    (root)
      cUSR="%F{red}"
      cPMT="%F{red}"
      ;;
    (*)
      cUSR="%F{green}%B"
      cPMT=""
      ;;
  esac
  cTIM="%F{cyan}%B"
  cPWD="%F{magenta}%B"
  cSTD="%b%f"
  export PS1="$cTIM%T$cSTD $cSRV%m$cSTD $cUSR%n$cSTD $cPWD%~$cSTD $cPMT%#$cSTD "
  export PS2="$cTIM%T$cSTD $cUSR>$cSTD $cPWD"

# SET PROPER ENCODINGS
  case ${TERM} in
    (cons25*) export LC_ALL=en_US.ISO8859-1 ;;
    (*)       export LC_ALL=en_US.UTF-8     ;;
  esac

# ALIASES
  alias rehash='hash -r'
  alias make='env LANG=C LC_ALL=C make'
  alias h='history'
  alias c='clear'
  alias vim='vim -i NONE'
  alias fetch='fetch -Rr --no-verify-peer --no-verify-hostname'
  alias wget='wget -c -t 0'

# LS/GLS/EXA
  case $( uname ) in
    (FreeBSD)
      if /usr/bin/env which exa 1> /dev/null 2> /dev/null
      then
        alias bls='/bin/ls -p -G -D "%Y.%m.%d %H:%M"'
        alias gls='gls -p --color=always --time-style=long-iso --group-directories-first --quoting-style=literal'
        alias ls='exa --time-style=long-iso --group-directories-first'
      elif /usr/bin/env which gls 1> /dev/null 2> /dev/null
      then
        alias bls='/bin/ls -p -G -D "%Y.%m.%d %H:%M"'
        alias ls=' gls -p --color=always --time-style=long-iso --group-directories-first --quoting-style=literal'
      else
        alias ls=' /bin/ls -p -G -D "%Y.%m.%d %H:%M"'
      fi
      ;;
    (OpenBSD)
      export PKG_PATH=http://ftp.openbsd.org/pub/OpenBSD/$( uname -r )/packages/$( uname -m )/
      [ -e /usr/local/bin/colorls ] && alias ls='/usr/local/bin/colorls -G'
      ;;
    (Linux)
      if /usr/bin/env which exa 1> /dev/null 2> /dev/null
      then
        alias gls='ls -p --color=always --time-style=long-iso --group-directories-first --quoting-style=literal'
        alias ls='exa --time-style=long-iso --group-directories-first'
      else
        alias ls='ls -p --color=always --time-style=long-iso --group-directories-first --quoting-style=literal'
      fi
      ;;
  esac
  alias la='ls -A'
  alias ll='ls -l'
  alias exa='exa --time-style=long-iso --group-directories-first'

If for any reason WordPress would mess the above config up here is the plain text version – https://raw.githubusercontent.com/vermaden/scripts/master/zshrc – available from my GitHub scripts repository.

While its comments generally say a lot about that is happening there I will also add several notes here.

I have left disabled the UPPERCASE from/to lowercase transparent translation because while it helped at about 10% of times it really pissed me off with pointless autocomplete suggestions the 90% of the time. If your work/complete schema is different the enable and test it. Maybe it will suit you better then me. Below is the part I am talking about – in enabled form.

# ZSH COMPLETION CASE (IN)SENSITIVE
  zstyle ':completion:*' matcher-list 'm:{a-zA-Z}={A-Za-z}'

At the end of the config you will find ‘casting’ for the best ls(1) solution existing in a system. After trying various listing commands such as:

  • FreeBSD ls(1) command
  • Linux ls(1) (known as gls(1) under FreeBSD)
  • New exa(1) command
  • New lsd(1) command

I have abandoned lsd(1) as besides colors its close to useless to use exa(1) as primary listing command. The second one that I recommend (that may be a surprise to FreeBSD users) would be the Linux ls(1) command from sysutils/coreutils package on FreeBSD. The last ‘resort’ command would be the FreeBSD ls(1) command as documented in the config. Why you should ask? The answer is quite simple – the directory listing. Both exa(1) and gls(1) have options to list directories (and what is more important SYMLINKS to directories) first. The FreeBSD ls(1) not only does not list symlinks to directories first – it also treat any directory as any other object and just list directories and symlinks put somewhere there withing all other files. Its unacceptable for me. Its just a messy pointless output. As much as I like and respect FreeBSD UNIX this is just plain fucking stupid. No matter how much history is in it.

Here is the comparison between them. I also wanted to show you the long listing (with -l option obviously) but its the same ‘not dirs first’ behavior for the FreeBSD ls(1) so not need for that.

zsh.ls

User Config

I will not add the ‘user’ part of my zsh(1) config and add some comments below.

# IMPORT DOAS/SUDO
  if [ -f ~/.zshrc.DOAS.SUDO ]
  then
    source ~/.zshrc.DOAS.SUDO
  else
    echo "NOPE: file ~/.zshrc.DOAS.SUDO absent."
  fi

# BASICS
  export PATH=${PATH}:~/scripts:~/scripts/bin:~/.cargo/bin
  export EDITOR=vi
  export VISUAL=vi
  export BROWSER=firefox
  export MANWIDTH=tty
  export ENV=${HOME}/.shrc
  export IFCONFIG_FORMAT=inet:cidr
  export LC_COLLATE=C

# BASICS DESKTOP
  export DISPLAY=:0
  export MOZ_DISABLE_IMAGE_OPTIMIZE=1
  export _JAVA_OPTIONS='-Dawt.useSystemAAFontSettings=on'
  export NO_AT_BRIDGE=1

# ENABLE ICONS IN exa(1)
  case ${TERM} in
    (rxvt)   : ;;
    (xterm*) : ;;
    (*)      alias exa='exa --icons' ;;
  esac

# ALIASES
  alias Grep=grep
  alias grpe=grep
  alias grepMAC='grep -i -E "[0-9a-f]{2}\:[0-9a-f]{2}\:[0-9a-f]{2}\:[0-9a-f]{2}\:[0-9a-f]{2}\:[0-9a-f]{2}"'
  alias grepIP='grep -E "([0-9]+\.){3}[0-9]+"'
  alias cls='printf "\033[H\033[J"'
  alias e=exa
  alias bat='bat --color=always'
  alias x='xinit ~/.xinitrc -- -dpi 75 -nolisten tcp 1> /dev/null 2> /dev/null'
  alias ffmpeg='ffmpeg -hide_banner'
  alias mupdf='mupdf -r 120'
  alias tac='tail -r'
  alias lsof='lsof -w'
  alias less='less -r --chop-long-lines'
  alias more='less -r --chop-long-lines'
  alias pstree='pstree -g 2'
  alias lupe='lupe -noshape -mag 2 -nohud -geometry 300x200 -noreticle -noiff'
  alias parallel='parallel --no-notice --progress -j 3'
  alias pv='pv -t -r -a -b -W -B 1048576'
  alias caja='caja --browser --no-desktop'
  alias evince=atril
  alias we="curl -4 http://wttr.in/Lodz\?Q\?n 2> /dev/null | sed '\$d' | sed '\$d'"
  alias cclive='cclive -c'
  alias yu='youtube-dl -c -i -f best --skip-unavailable-fragments'
  alias aria2c='aria2c --file-allocation=none'
  alias dig=drill
  alias cssh='cssh -o "-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no"'
  alias ssh='ssh -o LogLevel=quiet -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no'
  alias feh="feh --scale-down \
                 --auto-rotate \
                 --auto-zoom \
                 --fontpath ~/.fonts \
                 --font       ubuntu/8 \
                 --menu-font  ubuntu/8 \
                 --title-font ubuntu/8"
  alias wget='wget -c --no-check-certificate \
                   -U "Opera/12.16 (X11; FreeBSD 13.0 amd64; U; en) Presto/3 Version/12"'
  alias scp='scp -o ControlMaster=yes \
                 -o ControlPath=/tmp/%r@%h:%p \
                 -o UserKnownHostsFile=/dev/null \
                 -o StrictHostKeyChecking=no'

# SHORT HISTORY ALIASES h() H()
  alias h='< ~/.zhistory grep -i'
  alias H='< ~/.zhistory grep'

# SHORT GREP FUNCTIONS
  alias g='grep -i'
  alias G='grep'

# SHORT QUERY FUNCTIONS q()
  q() {
    if [ ${#} -eq 1 ]
    then
      /bin/ls | grep --color -i ${1} 2> /dev/null
    else
      echo "usage: q string"
    fi
  }

# SHORT QUERY FUNCTIONS Q()
  Q() {
    if [ ${#} -eq 1 ]
    then
      /bin/ls | grep --color ${1} 2> /dev/null
    else
      echo "usage: Q string"
    fi
  }

# SHORT QUERY FUNCTIONS qq()
  qq() {
    if [ ${#} -eq 1 ]
    then
      find . \
        | grep -i ${1} 2> /dev/null \
        | cut -c 3-999 \
        | grep --color -i ${1} 2> /dev/null
    else
      echo "usage: qq string"
    fi
  }

# SHORT QUERY FUNCTIONS QQ()
  QQ() {
    if [ ${#} -eq 1 ]
    then
      find . \
        | grep ${1} 2> /dev/null \
        | cut -c 3-999 \
        | grep --color ${1} 2> /dev/null
    else
      echo "usage: QQ string"
    fi
  }

# FUNTIONS / INTELIGENT CD()
  dc() {
    if [ -f "${@}" ]
    then
      cd "${@%/*}"
      return 0
    fi

    if [ -d "${@}" ]
    then
      cd "${@}"
      return 0
    fi

    echo "${0}: no such file or directory: ${@}"
    return 1
  }

# FUNTIONS / PORTS / ports-check()
  ports-check() {
    CUT='Major OS version upgrade detected.'
  # ${CMD} nice -n 20 portsnap auto
    ${CMD} nice -n 20 gitup ports
    echo
    ${CMD} nice -n 20 portmaster -L --index-only \
      | grep -v "${CUT}" \
      | awk '/ [Nn]ew / { print substr($0,9,9999) }'
    echo
    VULNS=$( ${CMD} pkg audit -F 2>&1 | grep ' vulnerable' | sort -u | sed 's/\ is\ vulnerable://g' )
    echo Vulnerabilities:
    if [ "${VULNS}" = "" ]
    then
      echo None.
    else
      echo "${VULNS}"
    fi
    echo
    pkg updating \
      -d $( date -j -f "%s" "$( pkg query -a %t | grep -v "${CUT}" | sort | tail -1 )" "+%Y%m%d" )
  }

# FUNTIONS / PORTS / ports-rebuild()
  ports-rebuild() {
    # OPTIONS
    local PORTS='multimedia/ffmpeg'
  # local PORTS='multimedia/ffmpeg audio/lame sysutils/exfat-utils sysutils/fusefs-exfat'

    for PORT in ${PORTS}
    do
      ${CMD} pkg unlock -y ${PORT} 1> /dev/null 2> /dev/null
      ${CMD} idprio 10 env BATCH=yes DISABLE_VULNERABILITIES=yes make -C /usr/ports/${PORT} build deinstall install clean &
      MAKE=${!}
      ${CMD} rctl -a process:${MAKE}:pcpu:deny=40
      ${CMD} wait ${MAKE}
    # ${CMD} pkg lock -y ${PORT}
    done
  }

# FUNTIONS / PORTS / ports-build()
  ports-build() {
    case ${#} in
      (0) ${CMD} nice -n 20 portmaster -y --no-confirm -m 'BATCH=yes' -d -a ;;
      (*) ${CMD} nice -n 20 portmaster -y --no-confirm -m 'BATCH=yes' -d $@ ;;
    esac
    ${CMD} nice -n 20 find /var/db/pkg -type d -depth 1 -exec rm -rf {} ';' 2> /dev/null
  }

# FUNTIONS / PKG / pkg-defunct()
  pkg-defunct() {
    pkg version -Rl\? | cut -wf1
  }

# FUNTIONS / PORTS / pkg-version()
  pkg-version() {
    pkg version -I -l '<' | awk '{print $1}'
  }

# FUNTIONS / PORTS / pkg-size()
  pkg-size() {
    pkg info -as | sort -k 2 -h | tail -100
  }

# FUNTIONS / BMI
  bmi() { # 1=HEIGHT 2=WEIGHT
    if [ ${#} -ne 2 ]
    then
      echo "usage: $( basename ${0} ) HEIGHT WEIGHT"
      echo
      echo "table:"
      echo "  UNDER WEIGHT   LESS - 18.4"
      echo "  NORMAL WEIGHT  18.5 - 24.9"
      echo "  OVER WEIGHT    25.0 - 29.9"
      echo "  OBESITY        30.0 - MORE"
      echo
      return 1
    fi
    local BMI=$( echo "${2} / ( ${1} * ${1} ) * 10000" | bc -l )
    printf "%.1f\n" "${BMI}"
  }

# FUNTIONS / BFP
  bfp() {
    if [ ${#} -ne 4 ]
    then
      echo "usage: $( basename ${0} ) HEIGHT WEIGHT AGE SEX"
      echo
      echo "SEX: f - female"
      echo "     m - male"
      return 1
    fi
    case ${4} in
      (m) SEX=1 ;;
      (f) SEX=0 ;;
    esac
    local BMI=$( echo "${2} / ( ${1} * ${1} ) * 10000" | bc -l )
    local BFP=$( echo "( 1.2 * ${BMI} ) + ( 0.23 * ${3} ) - ( 10.8 * ${SEX} ) - 5.4" | bc -l )
    printf "%.1f%%\n" "${BFP}"
  }

# FUNTIONS / BMR
  bmr() {
    if [ ${#} -ne 3 ]
    then
      echo "usage: $( basename ${0} ) WIEGHT HEIGHT AGE"
      echo
      return 1
    fi
    local RESULT=$( echo "( 10 * ${1} ) + ( 6.25 * ${2} ) - ( 5 * ${3} ) + 5" | bc -l )
    if echo ${RESULT} | grep -q '^\.'
    then
      echo -n 0
    fi
    echo ${RESULT} | awk -F '.' '{print $1}'
  }

# FUNTIONS / MATH
  math() {
    local SCALE=2
    local INPUT=$( echo "${@}" | tr 'x' '*' | tr ',' '.' )
    local RESULT=$( echo "scale=${SCALE}; ${INPUT}" | bc -l )
    if echo ${RESULT} | grep -q '^\.'
    then
      echo -n 0
    fi
    echo ${RESULT}
  }

# FUNTIONS / MAH2WH
  conv_mah_2_wh() {
    if [ ${#} -ne 2 ]
    then
      echo "usage: $( basename ${0} ) mAh V"
      echo
      return 1
    fi
    local MAH2WH=$( echo "${1} * ${2} / 1000" | bc -l )
    printf "%.1f Wh\n" "${MAH2WH}"
  }

# FUNTIONS / WH2MAH
  conv_wh_2_mah() {
    if [ ${#} -ne 2 ]
    then
      echo "usage: $( basename ${0} ) Wh V"
      echo
      return 1
    fi
    local WH2MAH=$( echo "${1} / ${2} * 1000" | bc -l )
    printf "%.1f mAh\n" "${WH2MAH}"
  }

# FUNTIONS / CM2IN
  conv_cm_2_in() {
    if [ ${#} -ne 1 ]
    then
      echo "usage: $( basename ${0} ) INCH"
      echo
      return 1
    fi
    local CM=$( echo "${1} / 2.54" | bc -l )
    printf "%.1f cm EQUALS %.1f inch(es)\n" "${1}" "${CM}"
  }

# FUNTIONS / IN2CM
  conv_in_2_cm() {
    if [ ${#} -ne 1 ]
    then
      echo "usage: $( basename ${0} ) INCH"
      echo
      return 1
    fi
    local INCH=$( echo "${1} * 2.54" | bc -l )
    printf "%.1f inch(es) EQUALS %.1f cm\n" "${1}" "${INCH}"
  }

# FUNTIONS / REMOVE SSH known_hosts KEY
  ssh_known_hosts_key_remove() {
    if [[ -z "${1}" ]]
    then
      echo "usage: ${0} [host]"
      echo "  Removes specified host from ~/.ssh/known_hosts file."
    else
      sed -i '' -e "/${1}/d" ${HOME}/.ssh/known_hosts
    fi
  }

# FUNTIONS / CAL
  cal() {
    if which gcal 1> /dev/null 2> /dev/null
    then
      local TEST="${@}"
      if [ "${TEST}" = "-3" ]
      then
        gcal -s 1 .  | sed 1,2d | sed 3d
      else
        gcal -s 1 ${@}
      fi
    else
      cal ${@}
    fi
  }

# FUNTIONS / DAY
  day() {
    if [ ${#} -eq 0 ]
    then
      echo "usage: ${0##*/} DAY-OF-MONTH"
      return 1
    fi
    cal $( date +%Y ) \
      | env GREP_COLOR="07;32" grep --color=always -EC 6 " $1 |^$1 | $1\$" \
      | env GREP_COLOR="07;33" grep --color=always -B2 -A6 -E 'Mo|Tu|We|Th|Fr|Sa|Su' \
      | grep -v -- --;
  }

# FUNTIONS / SSH-COPY-ID
  if ! which ssh-copy-id 1> /dev/null 2> /dev/null
  then
    ssh-copy-id() {
      echo 'INFO: ssh-copy-id(1) is not available'
      echo 'HINT: cat ~/.ssh/id_rsa.pub | ssh USER@HOST "cat >> ~/.ssh/authorized_keys"'
    }
  fi

If for any reason WordPress would mess the above config up here is the plain text version – https://raw.githubusercontent.com/vermaden/scripts/master/DOT.zshrc – available from my GitHub scripts repository.

I will not try to describe more useful parts of it. There are tons of aliases there from which these are more interesting ones.

Quickly grep(1) for IP or MAC address with grepIP or grepMAC functions respectively.

While most folks out there recommend the [CTRL]+[L] shortcut I really rarely use it. I know it and I generally advocate for those old UNIX shortcuts but for some reason the right [CTRL] key on my keyboard can not exist. I just do not use it at all. Its like in new condition on any keyboard I use. I should swap [CTRL] keys every quarter to make them look similarly used πŸ™‚

Because of that I often use ‘c‘ shortcut to clear the screen. As I was forced to use Windows in my earlier employer I also had The Microsoft equivalent for clearing the terminal – the cls command – thus you will also find an alias for that in my config – so called muscle memory is still strong πŸ™‚

Other aliases just have some arguments that are useful to add in 95% of cases.

Now some comment on the functions. There are for sure the Short Query Functions that I described in my Ghost in the Shell series. There is also additional dc alias to take me into directory where a file is. For example I have full path file under my X11 PRIMARY BUFFER. For example its /home/vermaden/gfx/wallpapers/amiga-500-grey.png value. I can now type cd and paste that buffer and then remove the amiga-500-grey.png characters with [BACKSPACE] key or type dc and then paste /home/vermaden/gfx/wallpapers/amiga-500-grey.png value and hit [ENTER] key. That alias(1) will now take me to the /home/vermaden/gfx/wallpapers/ dir.

There are several FreeBSD related commands also. Both pkg(8) or FreeBSD Ports related.

There are several that are health related such as BMI/BFP/BMR calculations that I sometimes use.

I really like the (and often use) the math function as it has the best of both worlds – the expr(1) and bc(1) commands.

There are also several functions related to conversions like converting the battery capacities between the Wh and mAh values or inches to centimeters conversions.

Similarly to the FreeBSD ls(1) command I also prefer to use the Linux (or should I say GNU) version of cal(1) command (known as gcal(1) in FreeBSD).

I also sometimes use the day function to highlight the exact day in the context of full year. Sometimes (quite rarely but still) its useful to know each occurrence of the 19 day of each month in current year. Below you will find screenshot with example.

zsh-day

You probably noticed the ~/.zshrc.DOAS.SUDO file at the beginning. Its about the detection of both sudo(8) and doas(1) supervisor commands. I prefer the more secure and simpler doas(1) command so when both are detected in the system then the doas(1) will be chosen as the right one.

Here are the ~/.zshrc.DOAS.SUDO contents.

% cat ~/.zshrc.DOAS.SUDO
SUDO_WHICH=0
SUDO=0
DOAS_WHICH=0
DOAS=1
ROOT=0

# CHECK doas(8) WITH which(1)
if which doas 1> /dev/null 2> /dev/null
then
  DOAS_WHICH=1
else
  DOAS_WHICH=0
fi

# CHECK sudo(8) WITH which(1)
if which sudo 1> /dev/null 2> /dev/null
then
  SUDO_WHICH=1
else
  SUDO_WHICH=0
fi

# CHECK USER WITH whoami(1)
if [ "$( whoami )" = "root" ]
then
  ROOT=1
fi

# CHOOSE ONE FROM doas(8) AND sudo(8)
if [ ${DOAS_WHICH} -eq 1 -o ${SUDO_WHICH} -eq 1 ]
then
  if [   ${DOAS} -eq 0 -a ${SUDO} -eq 1 -a ${SUDO_WHICH} -eq 1 ]
  then
    CMD=sudo
  elif [ ${DOAS} -eq 1 -a ${SUDO} -eq 0 -a ${DOAS_WHICH} -eq 1 ]
  then
    CMD=doas
  elif [ ${DOAS} -eq 1 -a ${SUDO} -eq 1 -a ${DOAS_WHICH} -eq 1 ]
  then
    CMD=doas
  fi
elif [ ${ROOT} -eq 1 ]
then
  CMD=''
else
  echo "NOPE: This script needs 'doas' or 'sudo' to work properly."
  exit 1
fi

unset SUDO_WHICH
unset DOAS_WHICH
unset ROOT

If for any reason WordPress would mess the above config up here is the plain text version – https://raw.githubusercontent.com/vermaden/scripts/master/DOT.zshrc.DOAS.SUDO – available from my GitHub scripts repository.

Summary

As the zsh(1) shell is very configurable there are probably at least dozen guides that make it better then me and in more depth but I just wanted to share all these with you as many of you asked what I actually use as my daily shell ‘driver’ setup.

Maybe you will be able to show me some other interesting zsh(1) tips s that would make it even more productive setup πŸ™‚

UPDATE 1 – fish(1) Coloring and Completion in zsh(1)

One of the things I likes about the fish(1) shell was its ‘predictive’ showing what command can be executed completing from command history … I now also have that in the zsh(1) shell with two additional packages from FreeBSD.

First you need to install shells/zsh-autosuggestions and shells/zsh-syntax-highlighting packages.

# pkg install -y \
shells/zsh-autosuggestions \
shells/zsh-syntax-highlighting
Updating FreeBSD repository catalogue... FreeBSD repository is up to date. All repositories are up to date. The following 2 package(s) will be affected (of 0 checked): New packages to be INSTALLED: zsh-autosuggestions: 0.7.0 zsh-syntax-highlighting: 0.7.1,1 Number of packages to be installed: 2 40 KiB to be downloaded. [1/2] Fetching zsh-autosuggestions-0.7.0.pkg: 100% 9 KiB 8.7kB/s 00:01 [2/2] Fetching zsh-syntax-highlighting-0.7.1,1.pkg: 100% 31 KiB 31.9kB/s 00:01 Checking integrity... done (0 conflicting) [1/2] Installing zsh-autosuggestions-0.7.0... [1/2] Extracting zsh-autosuggestions-0.7.0: 100% [2/2] Installing zsh-syntax-highlighting-0.7.1,1... [2/2] Extracting zsh-syntax-highlighting-0.7.1,1: 100%
===== Message from zsh-autosuggestions-0.7.0: -- Add the line below to your .zshrc to enable auto suggestions. source /usr/local/share/zsh-autosuggestions/zsh-autosuggestions.zsh
===== Message from zsh-syntax-highlighting-0.7.1,1: -- Add the line below to *the end of* your .zshrc to enable highlighting. source /usr/local/share/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh

The second part of the job is to add two additional scripts to your ~/.zshrc config file as shown below.

% tail -13 ~/.zshrc

# ADDITIONAL COMPLETIONS zsh-autosuggestions
if [ -e /usr/local/share/zsh-autosuggestions/zsh-autosuggestions.zsh ]
then
source /usr/local/share/zsh-autosuggestions/zsh-autosuggestions.zsh
fi

# ADDITIONAL COMPLETIONS zsh-syntax-highlighting
if [ -e /usr/local/share/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh ]
then
source /usr/local/share/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh
fi

Now your zsh(1) shell will color the commands and also underline the existing files/directories and also showing grey additional completion of last time usage from history.

zsh-fish-completions

Not sure if now zsh(1) makes ALL the bells and whistles that fish(1) shell does but its definitely very small difference now if you are a big fish(1) fan πŸ™‚

EOF

Ghost in the Shell – Part 6 – Learn Shell Scripting

The Ghost in the Shell series were about efficient working in the shell environment but one of the feats of any sysadmin profession is the shell scripting. It is often needed to ‘glue’ various solutions and technologies to work as ‘Business’ requires or to fill the gap where any solution is not available – or at least not for free. It also serves a growing role in the automation of various tasks. Today I will try to show you the basics of writing POSIX /bin/sh compatible shell scripts.

You may want to check other articles in the Ghost in the Shell series on the Ghost in the Shell – Global Page where you will find links to all episodes of the series along with table of contents for each episode’s contents.

Basics

In your own ‘yard’ you can use any shell language you want – there are many good interactive shells like zsh(1)/bash(1)/fish(1)/ksh(1) to name a few. Just keep in mind to stay away from csh(1)/tcsh(1) shells as they are mediocre at most in interactive mode and terrible for scripting. Its really pity that csh(1)/tcsh(1) shells are still used as the default FreeBSD shells today knowing that zsh(1) is available under MIT license and could be painlessly integrated into the FreeBSD Base System – but who I am to fix all the world’s problems … I just install zsh(1) from packages and live on.

By writing POSIX /bin/sh scripts you are making sure that they will run not only on bash(1) in Linux but also on all BSD systems and all other UNIX systems out there. Even the really old dinosaurs like HP-UX or AIX.

I always struggled to find good example for learning the shell scripting but recently I got one idea and we will follow it today.

For our ‘target’ I have chosen the kldstat(8) command from FreeBSD. Its output is far from perfect (from my perspective) with showing the Size column in hexadecimal values – while sysadmin expects values in (mega/giga/tera)bytes. Our task will be to parse that kldstat(8) output into something more human readable.

Lets check that kldstat(8) output then.

% kldstat | head
Id Refs Address                Size Name
 1  133 0xffffffff80200000  1f11f28 kernel
 2    1 0xffffffff82112000   67feb0 zfs.ko
 3    1 0xffffffff82792000    1abe8 geom_eli.ko
 4    3 0xffffffff82a3c000    56ec0 vboxdrv.ko
 5    2 0xffffffff82a93000     4240 vboxnetflt.ko
 6    3 0xffffffff82a98000     aac8 netgraph.ko
 7    1 0xffffffff82aa3000     31c8 ng_ether.ko
 8    1 0xffffffff82aa7000     55e0 vboxnetadp.ko
 9    1 0xffffffff82aad000   158458 i915kms.ko

Now what does 1f11f28 tell me about kernel for the Size column. Not much.

For a start I would like to print just the Size and Name columns in our script – we will call it kld.sh for the lack of better name and I will add version ‘tag’ to its name for each of our steps like kld.0.1.sh for first and ./kld.0.2.sh for the second one and so on.

There are many ways to parse that kldstat(8) output in our script but I will discuss two approaches here.

First is to get the /bin/sh output into variable and then parse it in a loop.

Second one to parse it in a loop in pipe after the command directly. I will use the second one here because the first one – with keeping then /bin/sh output in a variable – my be useful if we want to parse it more then once and as we will parse it only once then its pointless to ‘waste’ memory for that variable. Below you will find the first draft or kld.sh.

0.1

Our first 0.1 version has only the interpreter set at the beginning (the #!/bin/sh shebang) and the simple while read loop to get output of the kldstat(8) command and print it on the screen with shell builtin echo(1) command.

% cat ./kld.0.1.sh
#!/bin/sh

kldstat \
  | while read LINE
    do
      echo "${LINE}"
    done

Here is our script output – its generally identical as the kldstat(8) command.

% ./kld.0.1.sh | head
Id Refs Address                Size Name
1  133 0xffffffff80200000  1f11f28 kernel
2    1 0xffffffff82112000   67feb0 zfs.ko
3    1 0xffffffff82792000    1abe8 geom_eli.ko
4    3 0xffffffff82a3c000    56ec0 vboxdrv.ko
5    2 0xffffffff82a93000     4240 vboxnetflt.ko
6    3 0xffffffff82a98000     aac8 netgraph.ko
7    1 0xffffffff82aa3000     31c8 ng_ether.ko
8    1 0xffffffff82aa7000     55e0 vboxnetadp.ko
9    1 0xffffffff82aad000   158458 i915kms.ko

0.2

As we know that kldstat(8) has fixed number of columns we can read its more intelligently with variables names as its columns and print only Size and Name columns as we wanted it in the first place. We should also omit the first line of kldstat(8) output as we will be printing our own header for just Size and Name columns. We will achieve that with sed(1) command.

Here is out script after our improvements.

% cat kld.0.2.sh
#!/bin/sh

echo "SIZE NAME"
kldstat \
  | sed 1d \
  | while read ID REFS ADDRESS SIZE NAME
    do
      echo "${SIZE} ${NAME}"
    done

Here is its output at current early stage.

% ./kld.0.2.sh | head
SIZE NAME
1f11f28 kernel
67feb0 zfs.ko
1abe8 geom_eli.ko
56ec0 vboxdrv.ko
4240 vboxnetflt.ko
aac8 netgraph.ko
31c8 ng_ether.ko
55e0 vboxnetadp.ko
158458 i915kms.ko

As you can see the columns are not aligned so we can use column(1) command to make it look more like original command.

% ./kld.0.2.sh | column -t | head
SIZE     NAME
1f11f28  kernel
67feb0   zfs.ko
1abe8    geom_eli.ko
56ec0    vboxdrv.ko
4240     vboxnetflt.ko
aac8     netgraph.ko
31c8     ng_ether.ko
55e0     vboxnetadp.ko
158458   i915kms.ko

But typing that each time we execute our script can be PITA so we will now use printf(1) instead of echo(1) to print our output. We will also alight the first Size column to the right to make the command output even more human readable. We will sacrifice 8 places of width for the Size column (%8s) and the rest with aligned to left (%-s) for Name column.

0.3

Here is our improved script.

% cat kld.0.3.sh
#!/bin/sh

printf "%8s %-s\n" SIZE NAME
kldstat \
  | sed 1d \
  | while read ID REFS ADDRESS SIZE NAME
    do
      printf "%8s %-s\n" ${SIZE} ${NAME}
    done

Our output now looks like that one below.

% ./kld.0.3.sh | head
      SIZE NAME
   1f11f28 kernel
    67feb0 zfs.ko
     1abe8 geom_eli.ko
     56ec0 vboxdrv.ko
      4240 vboxnetflt.ko
      aac8 netgraph.ko
      31c8 ng_ether.ko
      55e0 vboxnetadp.ko
    158458 i915kms.ko

Better. Now we will improve two things. First we will start keeping our output format ("%8s %-s\n") in a separate variable and we will finally convert that hexadecimal values into decimal ones – to bytes – there are many ways to do that but I am leaning to use the printf(1) builtin because both of speed and it being available in the shell (builtin).

0.4

Here is the script.

% cat kld.0.4.sh
#!/bin/sh

FORMAT="%8s %-s\n"
printf "${FORMAT}" SIZE NAME
kldstat \
  | sed 1d \
  | while read ID REFS ADDRESS SIZE NAME
    do
      SIZE=$( printf "%d" 0x${SIZE} )
      printf "${FORMAT}" ${SIZE} ${NAME}
    done

And its output with bytes instead of hexadecimal values.

% ./kld.0.4.sh | head
      SIZE NAME
  32579368 kernel
   6815408 zfs.ko
    109544 geom_eli.ko
    356032 vboxdrv.ko
     16960 vboxnetflt.ko
     43720 netgraph.ko
     12744 ng_ether.ko
     21984 vboxnetadp.ko
   1410136 i915kms.ko


Now we have output in bytes and its nicely formatted. Its even easily sortable by the sort(1) command so its leaning nicely with UNIX principles.

% ./kld.0.4.sh | sort -n | head
      SIZE NAME
      8432 coretemp.ko
      8504 cd9660_iconv.ko
      8504 msdosfs_iconv.ko
      8504 udf_iconv.ko
      8576 smbus.ko
      8736 cpuctl.ko
      8800 pty.ko
      9000 lindebugfs.ko
      9024 uhid.ko

The next step would be to print that information in megabytes instead of just plain bytes. To convert bytes into kilobytes we need to divide our bytes value by 1024. To get the megabytes we need to do it twice. We will use the $(( ... )) syntax to use the shell builtin for simple math calculations instead of dropping that task to a subshell with $( ... ) syntax and external commands.

0.5

This is our ‘show in megabytes only’ script looks like.

% cat kld.0.5.sh
#!/bin/sh

FORMAT="%8s %-s\n"
printf "${FORMAT}" SIZE NAME
kldstat \
  | sed 1d \
  | while read ID REFS ADDRESS SIZE NAME
    do
      SIZE=$( printf "%d" 0x${SIZE} )
      SIZE=$(( ${SIZE} / 1024 / 1024 ))
      printf "${FORMAT}" ${SIZE} ${NAME}
    done

And here is its output.

% ./kld.0.5.sh | head
      SIZE NAME
        31 kernel
         6 zfs.ko
         0 geom_eli.ko
         0 vboxdrv.ko
         0 vboxnetflt.ko
         0 netgraph.ko
         0 ng_ether.ko
         0 vboxnetadp.ko
         1 i915kms.ko

That did not wend too well, didn’t it? Because many module use less then 1 megabytes of memory after being rounded to natural numbers its 0 megabytes value for many modules. We will try to use bc(1) calculator instead with up to tenths precision.

0.6

Here is out script after using bc(1) instead of using the $(( ... )) syntax with dividing.

% cat kld.0.6.sh
#!/bin/sh

FORMAT="%8s %-s\n"
printf "${FORMAT}" SIZE NAME
kldstat \
  | sed 1d \
  | while read ID REFS ADDRESS SIZE NAME
    do
      SIZE=$( printf "%d" 0x${SIZE} )
      SIZE=$( echo "scale=1; ${SIZE} / 1024 / 1024" | bc -l )
      printf "${FORMAT}" ${SIZE} ${NAME}
    done

And here is its output.

% ./kld.0.6.sh | head
      SIZE NAME
      31.0 kernel
       6.4 zfs.ko
        .1 geom_eli.ko
        .3 vboxdrv.ko
         0 vboxnetflt.ko
         0 netgraph.ko
         0 ng_ether.ko
         0 vboxnetadp.ko
       1.3 i915kms.ko

Far from ideal. The bc(1) output omits the leading zero if value is less then one. Seems that we can fix that with different printf(1) formatting. Lets try that. We will change from %8s (string) into %8.1f (float). That will also force us to use different formats for header and values so will stop using single FORMAT variable and we will use separate ones.

0.7

This is our current script state.

% cat kld.0.7.sh
#!/bin/sh

HEAD_FORMAT="%8s %-s\n"
LOOP_FORMAT="%8.1f %-s\n"
printf "${HEAD_FORMAT}" SIZE NAME
kldstat \
  | sed 1d \
  | while read ID REFS ADDRESS SIZE NAME
    do
      SIZE=$( printf "%d" 0x${SIZE} )
      SIZE=$( echo "scale=1; ${SIZE} / 1024 / 1024" | bc -l )
      printf "${LOOP_FORMAT}" ${SIZE} ${NAME}
    done

And its output.

% ./kld.0.7.sh | head
      SIZE NAME
      31.0 kernel
       6.4 zfs.ko
       0.1 geom_eli.ko
       0.3 vboxdrv.ko
       0.0 vboxnetflt.ko
       0.0 netgraph.ko
       0.0 ng_ether.ko
       0.0 vboxnetadp.ko
       1.3 i915kms.ko

Works as advertised. We can now think of something different. How about we will also add an argument to include the kernel and modules file sizes as well? Not very useful I think but for the the purpose of shell scripting learning process we will do it anyway. The first caveat here is that kernel modules are on two locations on FreeBSD. The Base System modules are kept at /boot/kernel location and the modules that were installed by pkg(8) packages (or from FreeBSD Ports) are located at /boot/modules place. To get their size we will use the stat(1) command. Similarly like with memory usage – we would like to have the output of kernel and its modules size in megabytes.

There are of course several ways to achieve that. Lets start with the longest most educational example below. I will just paste the fragment that gets that kernel or module size for the FILE column.

if [ -f /boot/modules/${NAME} ]
then
  FILE=$( stat -f %z /boot/modules/${NAME} )
fi

if [ -f /boot/kernel/${NAME} -a -z ${NAME} ]
then
  FILE=$( stat -f %z /boot/kernel/${NAME} )
fi

if [ "${FILE}" = "" ]
then
  FILE=-
fi

FILE=$( echo "scale=1; ${FILE} / 1024 / 1024" | bc -l )

One note about the [ "${FILE}" = "" ] syntax – in all old POSIX shells out there that I used /bin/sh always worked well with that syntax when FILE variable was empty or non existing. In a extreme example this one – [ "" = "" ] – works as desired. In case if you find yourself in a situation when this does not work in some POSIX /bin/sh implementation then use the most secure variant with additional same word added to both sides like that – [ "${FILE}test" = "test" ] – this way even the most badly written POSIX /bin/sh implementation will work πŸ™‚

It first checks the /boot/modules location for the module because I know a period of FreeBSD history in which the i915kms.ko module existed in both of these places and if you had them both then there is 99% percent chance that you are using the one installed by packages – that is why we try the third party modules first – then the ones from the Base System place. We also make sure that if for some reason the file will not be found the stat(1) command would not yield about its missing with 2> /dev/null at the end of command.

If we fail to find it under the third party modules then we will try the Base System location – but only when we did not find anything in the third party place – hence the additional test with -z ${NAME}.

For the record the syntax for these tests is:

  • for single test its like that: [ TEST ]
  • to test for both parameters (AND operator) its like that: [ TEST1 -a TEST ]
  • for only one of tests to pass (OR operator) its like that: [ TEST1 -o TEST ]

If we fail to find the file size then we set that to ‘‘ value.

At the end we divide by 1024 two times so we get megabytes from bytes.

This can be shortened to to take less place (and writing) into something like that.

[ -f /boot/modules/${NAME} ]              && FILE=$( stat -f %z /boot/modules/${NAME} 2> /dev/null )
[ -f /boot/kernel/${NAME} -a -z ${NAME} ] && FILE=$( stat -f %z /boot/kernel/${NAME}  2> /dev/null )
[ ${FILE} = "" ]                          && FILE=-
FILE=$( echo "scale=1; ${FILE} / 1024 / 1024" | bc -l )

The end result is the same but it requires less space and writing. I also added some spaces for ‘logical formatting’ to make it more readable.

There is also more extreme way to shorten this up while keeping the same logic – here it is.

FILE=$( stat -f %z /boot/kernel/${NAME}  2> /dev/null \
     || stat -f %z /boot/modules/${NAME} 2> /dev/null \
     || FILE=- )
FILE=$( echo "scale=1; ${FILE} / 1024 / 1024" | bc -l )

We use then || OR operator in the subshell to make that shorter and still keep it readable. This is the version that we will use in our script.

0.8

Lets see now how it looks after modifications.

% cat kld.0.8.sh
#!/bin/sh

HEAD_FORMAT="%8s %8s %-s\n"
LOOP_FORMAT="%8.1f %8.1f %-s\n"
printf "${HEAD_FORMAT}" SIZE FILE NAME
kldstat \
  | sed 1d \
  | while read ID REFS ADDRESS SIZE NAME
    do
      FILE=$( stat -f %z /boot/kernel/${NAME}  2> /dev/null \
           || stat -f %z /boot/modules/${NAME} 2> /dev/null \
           || FILE=- )
      FILE=$( echo "scale=1; ${FILE} / 1024 / 1024" | bc -l )
      SIZE=$( printf "%d" 0x${SIZE} )
      SIZE=$( echo "scale=1; ${SIZE} / 1024 / 1024" | bc -l )
      printf "${LOOP_FORMAT}" ${SIZE} ${FILE} ${NAME}
    done

And here is its output.

% ./kld.0.8.sh | head
    SIZE     FILE NAME
    31.0     27.7 kernel
     6.4      5.0 zfs.ko
     0.1      0.1 geom_eli.ko
     0.3      0.4 vboxdrv.ko
     0.0      0.0 vboxnetflt.ko
     0.0      0.1 netgraph.ko
     0.0      0.0 ng_ether.ko
     0.0      0.0 vboxnetadp.ko
     1.3      2.2 i915kms.ko

Its interesting to see that used memory and file size are different.

Another step would be printing also the summary of the used RAM for each column. This is where things get more interesting. The while loop is created in a pipe which means its in a subshell. This has some serious implications. Normally we would add two variables like SIZE_TOTAL and FILE_TOTAL to add each module size there and then after the loop ends just print the summary. Because the while loop is spawned as subshell these variables will vanish as soon as the loop will end its life and these variables would not exist (they existed only in that while subshell).

But fear not – there is very clever way with file descriptor to have these variables exist with their values after the while loop ends. Below you will find the shortened prototypes of our currently used ‘pipe’ way and the ‘descriptor’ way.

This is the way you already know.

kldstat \
  | sed 1d \
  | while read LINE
    do
      echo "${LINE}"
      TOTAL="Now You Don't."
    done

echo ${TOTAL}

When you will execute that you will NOT see the "Now You Don't." string.

Now this is the way to overcome that subshell limitation.

while read LINE
do
  echo "${LINE}"
  TOTAL="Now You See Me."
done << BSD
  $( kldstat | sed 1d )
BSD

echo ${TOTAL}

As you try it you will see the "Now You See Me." sign at the end.

This way we will provide summary for each column.

0.9

This is our code after our effort to add summary for the columns. You may noticed that we added the FILE_TOTAL and SIZE_TOTAL before the FILE and SIZE values are converted to megabytes. That ensures we are as accurate as possible. If we would just sum up the SIZE and FILE after they were converted to megabytes we would lost several bytes in the process.

% cat kld.0.9.sh
#!/bin/sh

HEAD_FORMAT="%8s %8s %-s\n"
LOOP_FORMAT="%8.1f %8.1f %-s\n"
printf "${HEAD_FORMAT}" SIZE FILE NAME
while read ID REFS ADDRESS SIZE NAME
do
  FILE=$( stat -f %z /boot/kernel/${NAME}  2> /dev/null \
       || stat -f %z /boot/modules/${NAME} 2> /dev/null \
       || FILE=- )
  FILE_TOTAL=$(( ${FILE_TOTAL} + ${FILE} ))
  FILE=$( echo "scale=1; ${FILE} / 1024 / 1024" | bc -l )
  SIZE=$( printf "%d" 0x${SIZE} )
  SIZE_TOTAL=$(( ${SIZE_TOTAL} + ${SIZE} ))
  SIZE=$( echo "scale=1; ${SIZE} / 1024 / 1024" | bc -l )
  printf "${LOOP_FORMAT}" ${SIZE} ${FILE} ${NAME}
done << BSD
  $( kldstat | sed 1d )
BSD
FILE_TOTAL=$( echo "scale=1; ${FILE_TOTAL} / 1024 / 1024" | bc -l )
SIZE_TOTAL=$( echo "scale=1; ${SIZE_TOTAL} / 1024 / 1024" | bc -l )
printf "${LOOP_FORMAT}" ${SIZE_TOTAL} ${FILE_TOTAL} TOTAL

This is how its execution looks like.

% ./kld.0.9.sh | (head -5; echo '(...)'; tail -5)
    SIZE     FILE NAME
    31.0     27.7 kernel
     6.4      5.0 zfs.ko
     0.1      0.1 geom_eli.ko
     0.3      0.4 vboxdrv.ko
(...)
     0.0      0.0 linsysfs.ko
     0.0      0.0 fdescfs.ko
     0.0      0.0 nullfs.ko
     0.0      0.0 acpi_ibm.ko
    40.9     39.5 TOTAL

As you can see I also used shell feature to pipe output into many commands at once – this allows us to show information that is most important to use – beginning and ending – for the summary.

We even can do nested piping as shown on the screenshot below.

lolcat

I deliberately used head(1) for entire guide because I have total of 42 kernel modules loaded. I did not wanted these outputs to overshadow our objective here. Here at the end I will show you complete output for the sake of it.

% kldstat | wc -l
      42

% ./kld.0.9.sh
    SIZE     FILE NAME
    31.0     27.7 kernel
     6.4      5.0 zfs.ko
     0.1      0.1 geom_eli.ko
     0.3      0.4 vboxdrv.ko
     0.0      0.0 vboxnetflt.ko
     0.0      0.1 netgraph.ko
     0.0      0.0 ng_ether.ko
     0.0      0.0 vboxnetadp.ko
     1.3      2.2 i915kms.ko
     0.4      0.8 drm.ko
     0.0      0.0 linuxkpi_gplv2.ko
     0.0      0.0 lindebugfs.ko
     0.0      0.1 fusefs.ko
     0.0      0.0 coretemp.ko
     0.0      0.0 sem.ko
     0.0      0.0 cpuctl.ko
     0.0      0.0 ichsmb.ko
     0.0      0.0 smbus.ko
     0.0      0.0 cuse.ko
     0.0      0.0 libiconv.ko
     0.0      0.0 cd9660_iconv.ko
     0.0      0.0 msdosfs_iconv.ko
     0.0      0.0 udf_iconv.ko
     0.0      0.0 udf.ko
     0.0      0.0 acpi_wmi.ko
     0.0      0.0 uhid.ko
     0.0      0.0 usbhid.ko
     0.0      0.0 hidbus.ko
     0.0      0.0 wmt.ko
     0.0      0.0 ums.ko
     0.1      0.2 ng_btsocket.ko
     0.0      0.0 ng_bluetooth.ko
     0.2      0.6 linux.ko
     0.0      0.1 linux_common.ko
     0.1      0.5 linux64.ko
     0.0      0.0 pty.ko
     0.0      0.0 linprocfs.ko
     0.0      0.0 linsysfs.ko
     0.0      0.0 fdescfs.ko
     0.0      0.0 nullfs.ko
     0.0      0.0 acpi_ibm.ko
    40.9     39.5 TOTAL

% kldstat
Id Refs Address                Size Name
 1  133 0xffffffff80200000  1f11f28 kernel
 2    1 0xffffffff82112000   67feb0 zfs.ko
 3    1 0xffffffff82792000    1abe8 geom_eli.ko
 4    3 0xffffffff82a3c000    56ec0 vboxdrv.ko
 5    2 0xffffffff82a93000     4240 vboxnetflt.ko
 6    3 0xffffffff82a98000     aac8 netgraph.ko
 7    1 0xffffffff82aa3000     31c8 ng_ether.ko
 8    1 0xffffffff82aa7000     55e0 vboxnetadp.ko
 9    1 0xffffffff82aad000   158458 i915kms.ko
10    1 0xffffffff82c06000    7f548 drm.ko
11    2 0xffffffff82c86000     cbc8 linuxkpi_gplv2.ko
12    2 0xffffffff82c93000     2328 lindebugfs.ko
13    1 0xffffffff82c96000    11f10 fusefs.ko
14    1 0xffffffff82ca8000     20f0 coretemp.ko
15    1 0xffffffff82cab000     39e8 sem.ko
16    1 0xffffffff82caf000     2220 cpuctl.ko
17    1 0xffffffff82cb2000     3250 ichsmb.ko
18    1 0xffffffff82cb6000     2180 smbus.ko
19    1 0xffffffff82cb9000     6730 cuse.ko
20    4 0xffffffff82cc0000     4798 libiconv.ko
21    1 0xffffffff82cc5000     2138 cd9660_iconv.ko
22    1 0xffffffff82cc8000     2138 msdosfs_iconv.ko
23    1 0xffffffff82ccb000     2138 udf_iconv.ko
24    1 0xffffffff82cce000     5a00 udf.ko
25    1 0xffffffff82cd4000     3378 acpi_wmi.ko
26    1 0xffffffff82cd8000     2340 uhid.ko
27    1 0xffffffff82cdb000     3380 usbhid.ko
28    1 0xffffffff82cdf000     31f8 hidbus.ko
29    1 0xffffffff82ce3000     3320 wmt.ko
30    1 0xffffffff82ce7000     4350 ums.ko
31    1 0xffffffff82cec000    1ce48 ng_btsocket.ko
32    1 0xffffffff82d09000     25a8 ng_bluetooth.ko
33    1 0xffffffff82d0c000    388f8 linux.ko
34    4 0xffffffff82d45000     db70 linux_common.ko
35    1 0xffffffff82d53000    30ac8 linux64.ko
36    1 0xffffffff82d84000     2260 pty.ko
37    1 0xffffffff82d87000     639c linprocfs.ko
38    1 0xffffffff82d8e000     3284 linsysfs.ko
39    1 0xffffffff82d92000     3530 fdescfs.ko
40    1 0xffffffff82d96000     4700 nullfs.ko
41    1 0xffffffff82d9b000     41d8 acpi_ibm.ko

Summary

This concludes this Ghost in the Shell episode.

Feel free to share your scripting habits and spells πŸ™‚

EOF

Ghost in the Shell – Part 5

The Ghost in the Shell series were quite neglected while I was busy writing about other things. Its about time to continue the series. I hope you are not mad at me because of it. Here are another few things that I think some of you may find useful.

You may want to check other articles in the Ghost in the Shell series on the Ghost in the Shell – Global Page where you will find links to all episodes of the series along with table of contents for each episode’s contents.

Less More Useful

From all less(1) command line options I find these very handy.

Often when you pass some command output to less(1) you loose color. To keep color in the less(1) output use --raw-control-chars (or -r for short equivalent) option.

The other useful less(1) option I find useful is --chop-long-lines (or -S for short equivalent) which prevents line wrapping. You can of course scroll horizontally to see what does not fit on the screen.

While less(1) is a command line program it also has a very nice --mouse option – with this option you can scroll its output with your mouse wheel. How cool is that? You can even specify how many lines you want to scroll with --wheel-lines=n options where ‘n is as you probably guessed the number of scrolled lines.

It may be also useful to make less(1) quit if you want to display file that its contents fits in the current console screen – use --quit-if-one-screen for that.

Often people when they want to just view some config files they use vi(1) (or their other favorite ${EDITOR} that they use) – even if they do not intend to edit the file. Its better to open such file in less(1) and if you find out that you would want to edit the file hit the ‘v char while being at less(1) – it will open that file in your ${EDITOR} for editing.

You can also display line number in less(1) with --LINE-NUMBERS option (or use -N for shorter equivalent).

Detox These Filenames

Often when copying files from various sources the filenames may become corrupt in the process – mostly because of differences in encodings. To fix that very fast one may use detox(1) command. On FreeBSD systems its available as sysutils/detox package. Because the FILE you will be renaming almost for sure contains some special characters or spaces then its best to add quotation marks as shown below.

% detox "FILE"

Of course detox(1) renames one file at a time so to rename all files in the current directory we will use simple loop.

% for FILE in *; do detox "${FILE}"; done

If you want to also include subdirectories the do the following.

% find . -type f -exec detox {} ';'

If you do not want to limit yourself to files only (fix directories names also) then – as Mandalorian would say – this is the way.

% find . -exec detox {} ';'

Man Up the Info Pages

In the learning process of mastering UNIX systems one has to get used to reading man(1) pages and often getting back to re-reading them when needed. Like with many other things the GNU folks wanted to do things in their own way – seems they did not liked the man(1) pages that much as they created info(1) pages as an alternative. I dunno about you but IMHO info(1) pages does not feel like the UNIX way … maybe it’s because GNU is a recursive acronym for GNU IS NOT UNIX πŸ™‚

However there is an elegant way to convert any info(1) page into man(1) page by piping the info(1) page output into less(1) command – or other ${PAGER} that you use.

% info ls | less

Real UNIX Sorting

After you setup your UNIX environment the LC_ALL environment variable is mostly set to some UTF variant – like en_us.utf-8 for example. That has implications as names of files and directories are now sorted case insensitively. To get back to original case sensitive UNIX sorting you can use the LC_ALL variable set ‘C‘. You can use that on the fly or make it permanent by adding it to your shell configuration. For example with ls(1) command shown below.

% ls -1
FreeBSD.org
kernel.org
Linux.com
NetBSD.org
openbsd.org
X11.org
xorg.conf

% env LC_ALL=C ls -1
FreeBSD.org
Linux.com
NetBSD.org
X11.org
kernel.org
openbsd.org
xorg.conf

Faster Better Uptime

When you want check for how long system was running we usually use uptime(1) command.

% uptime
8:15PM  up 5 days,  4:42, 4 users, load averages: 0.71, 0.76, 0.82

But you can type just one letter instead of six and get even more info – the w(1) command. It also includes information about other active sessions to this system – which comes handy because you want to know if someone else can try to fix or configure the same things as you intend to.

% w
8:15PM  up 5 days,  4:42, 4 users, load averages: 0.77, 0.78, 0.83
USER       TTY      FROM    LOGIN@  IDLE WHAT
vermaden   pts/0    :0     Thu10PM  3:09 -zsh (zsh)
szasstam   pts/1    :0     Sun08PM 1day  -zsh (zsh)
edwin      pts/2    :0      7:04PM     - -zsh (zsh)
larloch    pts/3    :0      7:56PM     - w

Filter Huge Files

When you start grep(1) to filter really big file – like several gigabytes in size for example – the grep(1) command uses locale from LC_ALL and LANG variables – which as you probably guess right know from the context of this sentence – slows things down.

You can modify both LC_ALL and LANG on the fly to ‘C‘ value to make that grep(1) really fast – and when I mean fast I mean sometimes you will gain several orders of magnitude.

% env LC_ALL=C LANG=C grep string HUGEFILE

That is all for this episode. Hope you liked it.

EOF

Ghost in the Shell – Part 4

Long time no see. Its been a while since last post in the Ghost in the Shell series. Its also exactly one full year since I started this blog – from the first Ghost in the Shell series article – the Part 1 – that was published on 2018/03/15 day.

Today I would like to show you new pack of useful tricks and features for productive terminal/shell use. Lets start with something simple yet useful.

You may want to check other articles in the Ghost in the Shell series on the Ghost in the Shell – Global Page where you will find links to all episodes of the series along with table of contents for each episode’s contents.

Named Pipes

We all (or at least most :>) know and love pipes in UNIX. For the record – ls | grep match | awk '{print $3}' | sed 's/.jpg//g' – command ‘chains’ like that one πŸ™‚

What is a named pipe then? A manually defined pipe for special purposes. For example some applications – especially the so called Enterprise ones – often do not support UNIX pipes mechanisms – they only can dump something to a file. A great example of such Enterprise software is Oracle database whose dump command can only make dump to a file. With tool that supports UNIX pipes you would probably want to pipe that data to gzip(1)/xz(1) to compress it on the fly or even pipe it directly to ssh(1) to the Backup server for example, but not with Oracle.

This is where named pipes feature helps. We will create named pipe called /tmp/PIPE so Oracle’s dump command will be able to use it and on the other side of this pipe we will attach a pipe to gzip -9 command to compress that data on the fly.

Below example is from Linux system so mknod(1) command will be used. For example on FreeBSD you would use mkfifo(1) command for named pipe. Complete example of such named pipe is presented below.

root # cd /tmp
root # mknod /tmp/PIPE p
root # chown oracle:oinstall /tmp/PIPE
root # dd if=/tmp/PIPE bs=1M | gzip -9 > /mnt/oracle/oracle-database-backup.dmp.gz &

Now the /tmp/PIPE named pipe is ready to be used. When any process will start to write something to the /tmp/PIPE named pipe it will be automatically grabbed by dd(8) command and piped to the gzip(1) command that will compress that input and write it into the /mnt/oracle/oracle-database-backup.dmp.gz file.

Now we can start the Oracle dumping process with dump command.

root # su - oracle
oracle % dump file=/tmp/PIPE

When the dump command finishes its work you will find all your dumped data compressed in the /mnt/oracle/oracle-database-backup.dmp.gz file.

Other example of named pipes usage is my desktop dzen2 setup with unusual update schedule – described in detail in the FreeBSD Desktop – Part 13 – Configuration – Dzen2 article.

Modify Command Environment on the Fly

For most of the time we use export(1) builtin to export needed environment values that our command needs. You can then check what environment exported values are with the env(1) command of course … but you can use the same env(1) command to run any command with modified environment without exporting variables using export(1).

Here is brief example of this feature.

For the record – the gls(1) command is a GNU/Linux ls(1) command from sysutils/coreutils package/port but to make it work without name conflicts on FreeBSD where BSD ls(1) is also present it had to be renamed to gls(1).

% gls -l | head -1
total 8609K

% env LC_ALL=pl_PL.UTF-8 gls -l | head -1
razem 8609K

In the example above we run gls(1) command with default environment – I use en_US.UTF-8 locale daily. The second invocation with LC_ALL=pl_PL.UTF-8 modified environment made gls(1) command display its output in Polish (pl_PL.UTF-8) language. The word ‘razem‘ means ‘total‘ in Polish.

Other useful example may be using make(1) to build FreeBSD port with known vulnerabilities. By default FreeBSD’s build(7) system will not allow us to build such port (and that is good defaults) but if we know what we are doing we will use following spell.

# env DISABLE_VULNERABILITIES=yes make -C /usr/ports/security/bdes/ build install clean

Its also useful with commands that do not play well with UTF-8 input like tr(1) for example. When LC_ALL is set to en_US.UTF-8 it will throw an error upon as.

% tr -cd '0-9' < /dev/random | head -c 16
tr: Illegal byte sequence
%

We just wanted to generate random 16 numbers.

To make it work we will modify the LC_ALL environment for this invocation.

% env LC_ALL=C tr -cd '0-9' < /dev/random | head -c 16
9571949869123855
%

Much better πŸ™‚

Other example with timezones using date(1) command and TZ variable as shown in the example below.

% date
Fri Mar 15 14:03:38 CET 2019

% env TZ=Australia/Darwin date 
Fri Mar 15 22:35:26 ACST 2019

The Real Path

The symlinks with ln(1) are very useful for many ways, to organize stuff, for quick fixes, for versioning … you will find tons of other use cases.

There is just one problem, if you make to many levels or symlinks or its just too much nested you do not know where you are anymore … this is where the realpath(1) comes handy. No matter how many levels of links you have made, it will tell you the truth – what is the current real path. The pwd(1) command will not help you here thou.

Here is a short example how it works.

% pwd
/home/vermaden
% ln -s /home/vermaden ASD
% cd ASD
% pwd
/home/vermaden/ASD
% realpath
/home/vermaden

Browsing the PATH

Many times I wanted to ‘browse’ through the PATH to search for something. As you possibly know the PATH variable stores paths that are colon (:) separated.

You can redefine the IFS variable which by default contains space ‘ ‘ which will work as field delimited for the for loop.

Here is the example.

% export IFS=":"

% for I in $( echo ${PATH} ); do echo ${I}; done
/sbin
/bin
/usr/sbin
/usr/bin
/usr/local/sbin
/usr/local/bin 

% for I in $( echo ${PATH} ); do find "${I}" -name ifconfig; done
/sbin/ifconfig

The other way to do this is to use plain old tr tool to translate colons (:) into newlines (\n) so we will be able to use the while loop here.

Here is the tr(1) example.

% echo ${PATH} | tr ':' '\n' | while read I; do echo ${I}; done
/sbin
/bin
/usr/sbin
/usr/bin
/usr/local/sbin
/usr/local/bin

% echo ${PATH} | tr ':' '\n' | while read I; do find ${I} -name dd; done
/bin/dd

You can also achieve same thing using the Parameter Expansion in which we will change the colons (:) into newlines (\n) as shown in the example below.

% echo "${PATH//:/\n}"
/sbin
/bin
/usr/sbin
/usr/bin
/usr/local/sbin
/usr/local/bin

# echo "${PATH//:/\n}" | while read I; do find ${I} -name camcontrol; done
/sbin/camcontrol

Parameter Expansion

I will not show all possible Parameter Expansion methods – just the most useful ones.

The typical use is to get the extension of a file or to ’emulate’ basename(1) or dirname(1) commands – it will be faster to use Parameter Expansion instead of invoking these commands each time. Below are two tables showing what you will get from which Parameter Expansion method.

PARAMETER    RESULT                       DESC 
-----------  ---------------------------  --------------
${name}      kubica.polish.racing.legend  content
${name#*.}          polish.racing.legend  -
${name##*.}                       legend  extension
${name%%.*}  kubica                       -
${name%.*}   kubica.polish.racing         -

… and with slash (/) character.

PARAMETER    RESULT                       DESC 
-----------  ---------------------------  --------------
${name}      kubica/polish/racing/legend  content
${name#*/}          polish/racing/legend  -
${name##*/}                       legend  basename(1)
${name%%.*}  kubica                       root directory
${name%/*}   kubica/polish/racing         dirname(1)

You can also use Parameter Expansion methods to grab the protocol from an URL like shown below.

% URL="https://vermaden.wordpress.com"

% echo "${URL%%/*}"
https:

Sort Human Readable Values

Its simple and easy to sort just numerical values, we use sort -n for that – but values sometimes comes in human readable form like 4G, 350M and 120K. To sort these properly you will have to use sort -h flag as shown in the example below.

% du -sh /usr/*
102M    /usr/bin
228G    /usr/home
9.0M    /usr/include
 53M    /usr/lib
 43M    /usr/lib32
116K    /usr/libdata
1.9M    /usr/libexec
365M    /usr/local
512B    /usr/obj
9.5M    /usr/sbin
 39M    /usr/share
251K    /usr/tests

% du -sh /usr/* | sort -h
512B    /usr/obj
116K    /usr/libdata
251K    /usr/tests
1.9M    /usr/libexec
9.0M    /usr/include
9.5M    /usr/sbin
 39M    /usr/share
 43M    /usr/lib32
 53M    /usr/lib
102M    /usr/bin
365M    /usr/local
228G    /usr/home

If the values are in the first column then its simple but what to do when the values are not in the first column? You will use -k parameter of sort(1) which takes which column to sort as argument. Needed example below sorted bu human readable values and on the second USED column.

% zfs list | sort -h -k 2
NAME                         USED  AVAIL  REFER  MOUNTPOINT
local/usr/obj                 88K   130G    88K  /usr/obj
local/var/cache/pkg          128K   130G   128K  /var/cache/pkg
local/var/cache              216K   130G    88K  none
local/var                    304K   130G    88K  none
sys/ROOT/11.1-RELEASE        482M  2.39G  6.04G  /
local/usr/ports              729M   130G   729M  /usr/ports
local/jail/nextcloud         927M   130G   897M  /jail/nextcloud
local/jail                  1.00G   130G   100M  /jail
local/usr/src               1.28G   130G  1.28G  /usr/src
local/usr                   1.99G   130G    88K  none
sys/ROOT/11.2-RELEASE       8.69G  2.39G  7.10G  /
sys/ROOT                    9.16G  2.39G    88K  none
sys                         9.17G  2.39G    88K  none
local/home                   281G   130G   281G  /home
local                        288G   130G    88K  none

Write a File from vi(1) with Different Rights

How many times you have opened a system configuration file like /etc/sysctl.conf or /etc/fstab in your favorite vi(1) editor, made some changes and then when you wanted to save it – no luck – you are trying to write to file owned by root with regular user … the Read-only file, not written; use ! to override. message will be displayed. Of course you can save that file somewhere else like your home directory and them move it with doas(1)/sudo(8)/su(8) help to original location and fix its rights … or you may do that in one step instead.

After opening a file with vi(1) and some changes to write a file with doas(1)/sudo(8) rights you just need to type this.

:w !doas tee %

Then exit the vi(1) editor with force.

:q!

Here is how it looks in the editor.

:w !doas tee %

+=+=+=+=+=+=+=+
File contents are displayed here.

Press any key to continue [: to enter more ex commands]: [ENTER]

Here is the ‘legend’ for that spell.

:      vi(1) prompt
w      write a file
!doas  invoke doas(1) command
tee    command that will be started using doas(1) command
%      tells vi(1) to use current filename

In this process the current vi(1) contents will be redirected using tee(1) with doas(1) rights to the current (open that you opened) filename.

Of course it also works in vim(1) or neovim(1) and if sudo(8) is your poison then just use sudo instead doas(1) there.

Search Contents of PDF Files

We all love plain text files then they can be searched using grep(1) for data that is interesting for us … but grep(1) does not work with PDF files … or should I say its pointless/useless to use grep(1) to search PDF files. Fortunately pdfgrep(1) command exists and works beautifully with PDF files – including colored output.

Recently FreeBSD Journal has been made free and you will like to search for bhyve articles in FreeBSD Journal issues then this is the command for you.

% cd books/unix-bsd-journal
% exa
FreeBSD Journal - 2014-01-02.pdf FreeBSD Journal - 2016-09-10.pdf
FreeBSD Journal - 2014-03-04.pdf FreeBSD Journal - 2016-11-12.pdf
FreeBSD Journal - 2014-05-06.pdf FreeBSD Journal - 2017-01-02.pdf
FreeBSD Journal - 2014-07-08.pdf FreeBSD Journal - 2017-03-04.pdf
FreeBSD Journal - 2014-09-10.pdf FreeBSD Journal - 2017-05-06.pdf
FreeBSD Journal - 2014-11-12.pdf FreeBSD Journal - 2017-07-08.pdf
FreeBSD Journal - 2015-01-02.pdf FreeBSD Journal - 2017-09-10.pdf
FreeBSD Journal - 2015-03-04.pdf FreeBSD Journal - 2017-11-12.pdf
FreeBSD Journal - 2015-05-06.pdf FreeBSD Journal - 2018-01-02.pdf
FreeBSD Journal - 2015-07-08.pdf FreeBSD Journal - 2018-03-04.pdf
FreeBSD Journal - 2015-09-10.pdf FreeBSD Journal - 2018-05-06.pdf
FreeBSD Journal - 2015-11-12.pdf FreeBSD Journal - 2018-07-08.pdf
FreeBSD Journal - 2016-01-02.pdf FreeBSD Journal - 2018-09-10.pdf
FreeBSD Journal - 2016-03-04.pdf FreeBSD Journal - 2018-11-12.pdf
FreeBSD Journal - 2016-05-06.pdf FreeBSD Journal - 2019-01-02.pdf
FreeBSD Journal - 2016-07-08.pdf

% pdfgrep -i -n bhyve *.pdf
FreeBSD Journal - 2014-01-02 - Old Release.pdf:6: machine hypervisors, such as BHy
FreeBSD Journal - 2014-01-02 - Old Release.pdf:6: BHyVe
FreeBSD Journal - 2014-01-02 - Old Release.pdf:6: BHyVe IS THE BSD Hypervisor, de
FreeBSD Journal - 2014-01-02 - Old Release.pdf:6: Grehan and Neel Natu. The desig
FreeBSD Journal - 2014-01-02 - Old Release.pdf:6: BHyVe requires Intel CPUs w
FreeBSD Journal - 2014-01-02 - Old Release.pdf:6: BHyVe appeared in FreeBSD 1
FreeBSD Journal - 2014-01-02.pdf:42: machine hypervisors, such as BHyVe, Virtual
FreeBSD Journal - 2014-01-02.pdf:42: BHyVe e d
FreeBSD Journal - 2014-01-02.pdf:42: BHyVe IS THE BSD Hypervisor, developed by P
FreeBSD Journal - 2014-01-02.pdf:42: Grehan and Neel Natu. The design goal of BH
FreeBSD Journal - 2014-01-02.pdf:42: BHyVe requires Intel CPUs with VT-x and
FreeBSD Journal - 2014-01-02.pdf:42: BHyVe appeared in FreeBSD 10-CURRENT in
(...)

Here is how it looks in the xterm(1) terminal.

xterm-pdfgrep.png

Hope that today’s pack of spells will end up useful for you.

EOF

GlusterFS Cluster on FreeBSD with Ansible and GNU Parallel

Today I would like to present an article about setting up GlusterFS cluster on a FreeBSD system with Ansible and GNU Parallel tools.

gluster-logo.png

To cite Wikipedia “GlusterFS is a scale-out network-attached storage file system. It has found applications including cloud computing, streaming media services, and content delivery networks.” The GlusterFS page describes it similarly “Gluster is a scalable, distributed file system that aggregates disk storage resources from multiple servers into a single global namespace.”

Here are its advantages:

  • Scales to several petabytes.
  • Handles thousands of clients.
  • POSIX compatible.
  • Uses commodity hardware.
  • Can use any ondisk filesystem that supports extended attributes.
  • Accessible using industry standard protocols like NFS and SMB.
  • Provides replication/quotas/geo-replication/snapshots/bitrot detection.
  • Allows optimization for different workloads.
  • Open Source.

Lab Setup

It will be entirely VirtualBox based and it will consist of 6 hosts. To not create 6 same FreeBSD installations I used 12.0-RELEASE virtual machine image available from the FreeBSD Project directly:

There are several formats available – qcow2/raw/vhd/vmdk – but as I will be using VirtualBox I used the VMDK one.

I will use different prompts depending on where the command is executed to make the article more readable. Also then there is ‘%‘ at the prompt then a regular user is needed and if there is ‘#‘ at the prompt then a superuser is needed.

gluster1 #    // command run on the gluster1 node
gluster* #    // command run on all gluster nodes
client #      // command run on gluster client
vbhost %      // command run on the VirtualBox host

Here is the list of the machines for the GlusterFS cluster:

10.0.10.11 gluster1
10.0.10.12 gluster2
10.0.10.13 gluster3
10.0.10.14 gluster4
10.0.10.15 gluster5
10.0.10.16 gluster6

Each VirtualBox virtual machine for FreeBSD is the default one (as suggested in the VirtualBox wizard) with 512 MB RAM and NAT Network as shown on the image below.

virtualbox-freebsd-gluster-host.jpg

Here is the configuration of the NAT Network on VirtualBox.

virtualbox-nat-network.jpg

The cloned/copied FreeBSD-12.0-RELEASE-amd64.vmdk image will need to have different UUIDs so we will use VBoxManage internalcommands sethduuid command to achieve this.

vbhost % for I in $( seq 6 ); do cp FreeBSD-12.0-RELEASE-amd64.vmdk    vbox_GlusterFS_${I}.vmdk; done
vbhost % for I in $( seq 6 ); do VBoxManage internalcommands sethduuid vbox_GlusterFS_${I}.vmdk; done

To start the whole GlusterFS environment on VirtualBox use these commands.

vbhost % VBoxManage list vms | grep GlusterFS
"FreeBSD GlusterFS 1" {162a3b6f-4ec9-4709-bff8-162b0c8c9c41}
"FreeBSD GlusterFS 2" {2e30326c-ac5d-41d2-9b28-483375df38f6}
"FreeBSD GlusterFS 3" {6b2747ab-3ec6-4b1a-a28e-5d871d7891b3}
"FreeBSD GlusterFS 4" {12379cf8-31d9-4ff1-9945-465fc3ed15f0}
"FreeBSD GlusterFS 5" {a4b0d515-5924-4517-9052-df238c366f2b}
"FreeBSD GlusterFS 6" {66621755-1b97-4486-aa15-a7bec9edb343}

Check which GlusterFS machines are running.

vbhost % VBoxManage list runningvms | grep GlusterFS
vbhost %

Starting of the machines in VirtualBox Headless mode in parallel.

vbhost % VBoxManage list vms \
           | grep GlusterFS \
           | awk -F \" '{print $2}' \
           | while read I; do VBoxManage startvm "${I}" --type headless & done

After that command you should see these machines running.

vbhost % VBoxManage list runningvms
"FreeBSD GlusterFS 1" {162a3b6f-4ec9-4709-bff8-162b0c8c9c41}
"FreeBSD GlusterFS 2" {2e30326c-ac5d-41d2-9b28-483375df38f6}
"FreeBSD GlusterFS 3" {6b2747ab-3ec6-4b1a-a28e-5d871d7891b3}
"FreeBSD GlusterFS 4" {12379cf8-31d9-4ff1-9945-465fc3ed15f0}
"FreeBSD GlusterFS 5" {a4b0d515-5924-4517-9052-df238c366f2b}
"FreeBSD GlusterFS 6" {66621755-1b97-4486-aa15-a7bec9edb343}

Before we will try connect to our FreeBSD machines we need to make the minimal network configuration. Each FreeBSD machine will have such minimal /etc/rc.conf file as shown example for gluster1 host.

gluster1 # cat /etc/rc.conf
hostname=gluster1
ifconfig_DEFAULT="inet 10.0.10.11/24 up"
defaultrouter=10.0.10.1
sshd_enable=YES

For the setup purposes we will need to allow root login on these FreeBSD GlusterFS machines with PermitRootLogin yes option in the /etc/ssh/sshd_config file. You will also need to restart the sshd(8) service after the changes.

gluster1 # grep '^PermitRootLogin' /etc/ssh/sshd_config
PermitRootLogin yes
# service sshd restart

By using NAT Network with Port Forwarding the FreeBSD machines will be accessible on the localhost ports. For example the gluster1 machine will be available on port 2211, the gluster2 machine will be available on port 2212 and so on. This is shown in the sockstat utility output below.

vbhost % sockstat -l4
USER     COMMAND    PID   FD PROTO  LOCAL ADDRESS         FOREIGN ADDRESS
vermaden VBoxNetNAT 57622 17 udp4   *:*                   *:*
vermaden VBoxNetNAT 57622 19 tcp4   *:2211                *:*
vermaden VBoxNetNAT 57622 20 tcp4   *:2212                *:*
vermaden VBoxNetNAT 57622 21 tcp4   *:2213                *:*
vermaden VBoxNetNAT 57622 22 tcp4   *:2214                *:*
vermaden VBoxNetNAT 57622 23 tcp4   *:2215                *:*
vermaden VBoxNetNAT 57622 24 tcp4   *:2216                *:*
vermaden VBoxNetNAT 57622 28 tcp4   *:2240                *:*
vermaden VBoxNetNAT 57622 29 tcp4   *:9140                *:*
vermaden VBoxNetNAT 57622 30 tcp4   *:2220                *:*
root     sshd       96791 4  tcp4   *:22                  *:*

I think the corelation between IP address and the port on the host is obvious πŸ™‚

Here is the list of the machines with ports on localhost:

10.0.10.11 gluster1 2211
10.0.10.12 gluster2 2212
10.0.10.13 gluster3 2213
10.0.10.14 gluster4 2214
10.0.10.15 gluster5 2215
10.0.10.16 gluster6 2216

To connect to such machine from the VirtualBox host system you will need this command:

vbhost % ssh -l root localhost -p 2211

To not type that every time you need to login to gluster1 let’s make come changes to ~/.ssh/config file for convenience. This way it will be possible to login in very short way.

vbhost % ssh gluster1

Here is the modified ~/.ssh/config file.

vbhost % cat ~/.ssh/config
# GENERAL
  StrictHostKeyChecking no
  LogLevel              quiet
  KeepAlive             yes
  ServerAliveInterval   30
  VerifyHostKeyDNS      no

# ALL HOSTS SETTINGS
Host *
  StrictHostKeyChecking no
  Compression           yes

# GLUSTER
Host gluster1
  User root
  Hostname 127.0.0.1
  Port 2211

Host gluster2
  User root
  Hostname 127.0.0.1
  Port 2212

Host gluster3
  User root
  Hostname 127.0.0.1
  Port 2213

Host gluster4
  User root
  Hostname 127.0.0.1
  Port 2214

Host gluster5
  User root
  Hostname 127.0.0.1
  Port 2215

Host gluster6
  User root
  Hostname 127.0.0.1
  Port 2216

I assume that you already have some SSH keys generated (with ~/.ssh/id_rsa as private key) so lets remove the need to type password on each SSH login.

vbhost % ssh-copy-id -i ~/.ssh/id_rsa gluster1
Password for root@gluster1:

vbhost % ssh-copy-id -i ~/.ssh/id_rsa gluster2
Password for root@gluster2:

vbhost % ssh-copy-id -i ~/.ssh/id_rsa gluster3
Password for root@gluster3:

vbhost % ssh-copy-id -i ~/.ssh/id_rsa gluster4
Password for root@gluster4:

vbhost % ssh-copy-id -i ~/.ssh/id_rsa gluster5
Password for root@gluster5:

vbhost % ssh-copy-id -i ~/.ssh/id_rsa gluster6
Password for root@gluster6:

Ansible Setup

As we already have SSH integration now we will configure Ansible to connect to out ‘localhost’ ports for FreeBSD machines.

Here is the Ansible’s hosts file.

vbhost % cat hosts
[gluster]
gluster1 ansible_port=2211 ansible_host=127.0.0.1 ansible_user=root
gluster2 ansible_port=2212 ansible_host=127.0.0.1 ansible_user=root
gluster3 ansible_port=2213 ansible_host=127.0.0.1 ansible_user=root
gluster4 ansible_port=2214 ansible_host=127.0.0.1 ansible_user=root
gluster5 ansible_port=2215 ansible_host=127.0.0.1 ansible_user=root
gluster6 ansible_port=2216 ansible_host=127.0.0.1 ansible_user=root

[gluster:vars]
ansible_python_interpreter=/usr/local/bin/python2.7

Here is the listing of these machines using ansible command.

vbhost % ansible -i hosts --list-hosts gluster
  hosts (6):
    gluster1
    gluster2
    gluster3
    gluster4
    gluster5
    gluster6

Lets verify that out Ansible setup works correctly.

vbhost % ansible -i hosts -m raw -a 'echo' gluster
gluster1 | CHANGED | rc=0 >>



gluster3 | CHANGED | rc=0 >>



gluster2 | CHANGED | rc=0 >>



gluster5 | CHANGED | rc=0 >>



gluster4 | CHANGED | rc=0 >>



gluster6 | CHANGED | rc=0 >>

It works as desired.

We are not able to use Ansible modules other then Raw because by default Python is not installed on FreeBSD as shown below.

vbhost % ansible -i hosts -m ping gluster
gluster1 | FAILED! => {
    "changed": false,
    "module_stderr": "",
    "module_stdout": "/bin/sh: /usr/local/bin/python2.7: not found\r\n",
    "msg": "MODULE FAILURE\nSee stdout/stderr for the exact error",
    "rc": 127
}
gluster2 | FAILED! => {
    "changed": false,
    "module_stderr": "",
    "module_stdout": "/bin/sh: /usr/local/bin/python2.7: not found\r\n",
    "msg": "MODULE FAILURE\nSee stdout/stderr for the exact error",
    "rc": 127
}
gluster4 | FAILED! => {
    "changed": false,
    "module_stderr": "",
    "module_stdout": "/bin/sh: /usr/local/bin/python2.7: not found\r\n",
    "msg": "MODULE FAILURE\nSee stdout/stderr for the exact error",
    "rc": 127
}
gluster5 | FAILED! => {
    "changed": false,
    "module_stderr": "",
    "module_stdout": "/bin/sh: /usr/local/bin/python2.7: not found\r\n",
    "msg": "MODULE FAILURE\nSee stdout/stderr for the exact error",
    "rc": 127
}
gluster3 | FAILED! => {
    "changed": false,
    "module_stderr": "",
    "module_stdout": "/bin/sh: /usr/local/bin/python2.7: not found\r\n",
    "msg": "MODULE FAILURE\nSee stdout/stderr for the exact error",
    "rc": 127
}
gluster6 | FAILED! => {
    "changed": false,
    "module_stderr": "",
    "module_stdout": "/bin/sh: /usr/local/bin/python2.7: not found\r\n",
    "msg": "MODULE FAILURE\nSee stdout/stderr for the exact error",
    "rc": 127
}

We need to get Python installed on FreeBSD.

We will partially use Ansible for this and partially the GNU Parallel.

vbhost % ansible -i hosts --list-hosts gluster \
           | sed 1d \
           | while read I; do ssh ${I} env ASSUME_ALWAYS_YES=yes pkg install python; done
pkg: Error fetching http://pkg.FreeBSD.org/FreeBSD:12:amd64/quarterly/Latest/pkg.txz: No address record
A pre-built version of pkg could not be found for your system.
Consider changing PACKAGESITE or installing it from ports: 'ports-mgmt/pkg'.
Bootstrapping pkg from pkg+http://pkg.FreeBSD.org/FreeBSD:12:amd64/quarterly, please wait...

… we forgot about setting up DNS in the FreeBSD machines, let’s fix that.

It is as easy as executing echo nameserver 1.1.1.1 > /etc/resolv.conf command on each FreeBSD machine.

Lets verify what input will be sent to GNU Parallel before executing it.

vbhost % ansible -i hosts --list-hosts gluster \
           | sed 1d \
           | while read I; do echo "ssh ${I} 'echo nameserver 1.1.1.1 > /etc/resolv.conf'"; done
ssh gluster1 'echo nameserver 1.1.1.1 > /etc/resolv.conf'
ssh gluster2 'echo nameserver 1.1.1.1 > /etc/resolv.conf'
ssh gluster3 'echo nameserver 1.1.1.1 > /etc/resolv.conf'
ssh gluster4 'echo nameserver 1.1.1.1 > /etc/resolv.conf'
ssh gluster5 'echo nameserver 1.1.1.1 > /etc/resolv.conf'
ssh gluster6 'echo nameserver 1.1.1.1 > /etc/resolv.conf'

Looks reasonable, lets engage the GNU Parallel then.

vbhost % ansible -i hosts --list-hosts gluster \
           | sed 1d \
           | while read I; do echo "ssh ${I} 'echo nameserver 1.1.1.1 > /etc/resolv.conf'"; done | parallel

Computers / CPU cores / Max jobs to run
1:local / 2 / 2

Computer:jobs running/jobs completed/%of started jobs/Average seconds to complete
local:0/6/100%/1.0s

We will now verify that the DNS is configured properly on the FreeBSD machines.

vbhost % for I in $( jot 6 ); do echo -n "gluster${I} "; ssh gluster${I} 'cat /etc/resolv.conf'; done
gluster1 nameserver 1.1.1.1
gluster2 nameserver 1.1.1.1
gluster3 nameserver 1.1.1.1
gluster4 nameserver 1.1.1.1
gluster5 nameserver 1.1.1.1
gluster6 nameserver 1.1.1.1

Verification of the DNS by using ping(8) to test Internet connectivity.

vbhost % for I in $( jot 6 ); do echo; echo "gluster${I}"; ssh gluster${I} host freebsd.org; done

gluster1
freebsd.org has address 96.47.72.84
freebsd.org has IPv6 address 2610:1c1:1:606c::50:15
freebsd.org mail is handled by 10 mx1.freebsd.org.
freebsd.org mail is handled by 30 mx66.freebsd.org.

gluster2
freebsd.org has address 96.47.72.84
freebsd.org has IPv6 address 2610:1c1:1:606c::50:15
freebsd.org mail is handled by 30 mx66.freebsd.org.
freebsd.org mail is handled by 10 mx1.freebsd.org.

gluster3
freebsd.org has address 96.47.72.84
freebsd.org has IPv6 address 2610:1c1:1:606c::50:15
freebsd.org mail is handled by 30 mx66.freebsd.org.
freebsd.org mail is handled by 10 mx1.freebsd.org.

gluster4
freebsd.org has address 96.47.72.84
freebsd.org has IPv6 address 2610:1c1:1:606c::50:15
freebsd.org mail is handled by 30 mx66.freebsd.org.
freebsd.org mail is handled by 10 mx1.freebsd.org.

gluster5
freebsd.org has address 96.47.72.84
freebsd.org has IPv6 address 2610:1c1:1:606c::50:15
freebsd.org mail is handled by 10 mx1.freebsd.org.
freebsd.org mail is handled by 30 mx66.freebsd.org.

gluster6
freebsd.org has address 96.47.72.84
freebsd.org has IPv6 address 2610:1c1:1:606c::50:15
freebsd.org mail is handled by 10 mx1.freebsd.org.
freebsd.org mail is handled by 30 mx66.freebsd.org.

The DNS resolution works properly, now we will switch from the default quarterly pkg(8) repository to the latest one which has more frequent updates as the name suggests. We will need to use sed -i '' s/quarterly/latest/g /etc/pkg/FreeBSD.conf command on each FreeBSD machine.

Verification what will be sent to GNU Parallel.

vbhost % ansible -i hosts --list-hosts gluster \
           | sed 1d \
           | while read I; do echo "ssh ${I} 'sed -i \"\" s/quarterly/latest/g /etc/pkg/FreeBSD.conf'"; done
ssh gluster1 'sed -i "" s/quarterly/latest/g /etc/pkg/FreeBSD.conf'
ssh gluster2 'sed -i "" s/quarterly/latest/g /etc/pkg/FreeBSD.conf'
ssh gluster3 'sed -i "" s/quarterly/latest/g /etc/pkg/FreeBSD.conf'
ssh gluster4 'sed -i "" s/quarterly/latest/g /etc/pkg/FreeBSD.conf'
ssh gluster5 'sed -i "" s/quarterly/latest/g /etc/pkg/FreeBSD.conf'
ssh gluster6 'sed -i "" s/quarterly/latest/g /etc/pkg/FreeBSD.conf'

Let’s send the command to FreeBSD machines then.

vbhost % ansible -i hosts --list-hosts gluster \
           | sed 1d \
           | while read I; do echo "ssh $I 'sed -i \"\" s/quarterly/latest/g /etc/pkg/FreeBSD.conf'"; done | parallel

Computers / CPU cores / Max jobs to run
1:local / 2 / 2

Computer:jobs running/jobs completed/%of started jobs/Average seconds to complete
local:0/6/100%/1.0s

As shown below the latest repository is configured in the /etc/pkg/FreeBSD.conf file on each FreeBSD machine.

vbhost % ssh gluster3 tail -7 /etc/pkg/FreeBSD.conf
FreeBSD: {
  url: "pkg+http://pkg.FreeBSD.org/${ABI}/latest",
  mirror_type: "srv",
  signature_type: "fingerprints",
  fingerprints: "/usr/share/keys/pkg",
  enabled: yes
}

We may now get back to Python.

vbhost % ansible -i hosts --list-hosts gluster \
           | sed 1d \
           | while read I; do echo ssh ${I} env ASSUME_ALWAYS_YES=yes pkg install python; done
ssh gluster1 env ASSUME_ALWAYS_YES=yes pkg install python
ssh gluster2 env ASSUME_ALWAYS_YES=yes pkg install python
ssh gluster3 env ASSUME_ALWAYS_YES=yes pkg install python
ssh gluster4 env ASSUME_ALWAYS_YES=yes pkg install python
ssh gluster5 env ASSUME_ALWAYS_YES=yes pkg install python
ssh gluster6 env ASSUME_ALWAYS_YES=yes pkg install python

… and execution on the FreeBSD machines with GNU Parallel.

vbhost % ansible -i hosts --list-hosts gluster \ 
           | sed 1d \
           | while read I; do echo ssh ${I} env ASSUME_ALWAYS_YES=yes pkg install python; done | parallel

Computers / CPU cores / Max jobs to run
1:local / 2 / 2

Computer:jobs running/jobs completed/%of started jobs/Average seconds to complete
local:0/6/100%/156.0s

The Python packages and its dependencies are installed.

vbhost % ssh gluster3 pkg info
gettext-runtime-0.19.8.1_2     GNU gettext runtime libraries and programs
indexinfo-0.3.1                Utility to regenerate the GNU info page index
libffi-3.2.1_3                 Foreign Function Interface
pkg-1.10.5_5                   Package manager
python-2.7_3,2                 "meta-port" for the default version of Python interpreter
python2-2_3                    The "meta-port" for version 2 of the Python interpreter
python27-2.7.15                Interpreted object-oriented programming language
readline-7.0.5                 Library for editing command lines as they are typed

Now with Ansible Ping module works as desired.

% ansible -i hosts -m ping gluster
gluster1 | SUCCESS => {
"changed": false,
"ping": "pong"
}
gluster4 | SUCCESS => {
"changed": false,
"ping": "pong"
}
gluster5 | SUCCESS => {
"changed": false,
"ping": "pong"
}
gluster3 | SUCCESS => {
"changed": false,
"ping": "pong"
}
gluster2 | SUCCESS => {
"changed": false,
"ping": "pong"
}
gluster6 | SUCCESS => {
"changed": false,
"ping": "pong"
}

GlusterFS Volume Options

GlusterFS has a lot of options to setup the volume. They are described in the GlusterFS Administration Guide in the Setting up GlusterFS Volumes part. Here they are:

Distributed – Distributed volumes distribute files across the bricks in the volume. You can use distributed volumes where the requirement is to scale storage and the redundancy is either not important or is provided by other hardware/software layers.

Replicated – Replicated volumes replicate files across bricks in the volume. You can use replicated volumes in environments where high-availability and high-reliability are critical.

Distributed Replicated – Distributed replicated volumes distribute files across replicated bricks in the volume. You can use distributed replicated volumes in environments where the requirement is to scale storage and high-reliability is critical. Distributed replicated volumes also offer improved read performance in most environments.

Dispersed – Dispersed volumes are based on erasure codes, providing space-efficient protection against disk or server failures. It stores an encoded fragment of the original file to each brick in a way that only a subset of the fragments is needed to recover the original file. The number of bricks that can be missing without losing access to data is configured by the administrator on volume creation time.

Distributed Dispersed – Distributed dispersed volumes distribute files across dispersed subvolumes. This has the same advantages of distribute replicate volumes, but using disperse to store the data into the bricks.

Striped [Deprecated] – Striped volumes stripes data across bricks in the volume. For best results, you should use striped volumes only in high concurrency environments accessing very large files.

Distributed Striped [Deprecated] – Distributed striped volumes stripe data across two or more nodes in the cluster. You should use distributed striped volumes where the requirement is to scale storage and in high concurrency environments accessing very large files is critical.

Distributed Striped Replicated [Deprecated] – Distributed striped replicated volumes distributes striped data across replicated bricks in the cluster. For best results, you should use distributed striped replicated volumes in highly concurrent environments where parallel access of very large files and performance is critical. In this release, configuration of this volume type is supported only for Map Reduce workloads.

Striped Replicated [Deprecated] – Striped replicated volumes stripes data across replicated bricks in the cluster. For best results, you should use striped replicated volumes in highly concurrent environments where there is parallel access of very large files and performance is critical. In this release, configuration of this volume type is supported only for Map Reduce workloads.

From all of the above still supported the Dispersed volume seems to be the best choice. Like Minio Dispersed volumes are based on erasure codes.

As we have 6 servers we will use 4 + 2 setup which is logical RAID6 against these 6 servers. This means that we will be able to lost 2 of them without service outage. This also means that if we will upload 100 MB file to our volume we will use 150 MB of space across these 6 servers with 25 MB on each node.

We can visualize this as following ASCII diagram.

+-----------+ +-----------+ +-----------+ +-----------+ +-----------+ +-----------+
|  gluster1 | |  gluster2 | |  gluster3 | |  gluster4 | |  gluster5 | |  gluster6 |
|           | |           | |           | |           | |           | |           |
|    brick1 | |    brick2 | |    brick3 | |    brick4 | |    brick5 | |    brick6 |
+-----+-----+ +-----+-----+ +-----+-----+ +-----+-----+ +-----+-----+ +-----+-----+
      |             |             |             |             |             |
    25|MB         25|MB         25|MB         25|MB         25|MB         25|MB
      |             |             |             |             |             |
      +-------------+-------------+------+------+-------------+-------------+
                                         |
                                      100|MB
                                         |
                                     +---+---+
                                     | file0 |
                                     +-------+

Deploy GlusterFS Cluster

We will use gluster-setup.yml as our Ansible playbook.

Lets create something for the start, for example to always install the latest Python package.

vbhost % cat gluster-setup.yml
---
- name: Install and Setup GlusterFS on FreeBSD
  hosts: gluster
  user: root
  tasks:

  - name: Install Latest Python Package
    pkgng:
      name: python
      state: latest

We will now execute it.

vbhost % ansible-playbook -i hosts gluster-setup.yml

PLAY [Install and Setup GlusterFS on FreeBSD] **********************************

TASK [Gathering Facts] *********************************************************
ok: [gluster3]
ok: [gluster5]
ok: [gluster1]
ok: [gluster4]
ok: [gluster2]
ok: [gluster6]

TASK [Install Latest Python Package] *******************************************
ok: [gluster4]
ok: [gluster2]
ok: [gluster5]
ok: [gluster3]
ok: [gluster1]
ok: [gluster6]

PLAY RECAP *********************************************************************
gluster1                   : ok=2    changed=0    unreachable=0    failed=0
gluster2                   : ok=2    changed=0    unreachable=0    failed=0
gluster3                   : ok=2    changed=0    unreachable=0    failed=0
gluster4                   : ok=2    changed=0    unreachable=0    failed=0
gluster5                   : ok=2    changed=0    unreachable=0    failed=0
gluster6                   : ok=2    changed=0    unreachable=0    failed=0

We just installed Python on these machines no update was needed.

As we will be creating cluster we need to add time synchronization between the nodes of the cluster. We will use mose obvious solution – the ntpd(8) daemon that is in the FreeBSD base system. These lines are added to our gluster-setup.yml playbook to achieve this goal

  - name: Enable NTPD Service
    raw: sysrc ntpd_enable=YES

  - name: Start NTPD Service
    service:
      name: ntpd
      state: started

After executing the playbook again with the ansible-playbook -i hosts gluster-setup.yml command we will see additional output as the one shown below.

TASK [Enable NTPD Service] ************************************************
changed: [gluster2]
changed: [gluster1]
changed: [gluster4]
changed: [gluster5]
changed: [gluster3]
changed: [gluster6]

TASK [Start NTPD Service] ******************************************************
changed: [gluster5]
changed: [gluster4]
changed: [gluster2]
changed: [gluster1]
changed: [gluster3]
changed: [gluster6]

Random verification of the NTP service.

vbhost % ssh gluster1 ntpq -p
     remote           refid      st t when poll reach   delay   offset  jitter
==============================================================================
 0.freebsd.pool. .POOL.          16 p    -   64    0    0.000    0.000   0.000
 ntp.ifj.edu.pl  10.0.2.4         3 u    1   64    1  119.956  -345759  32.552
 news-archive.ic 229.30.220.210   2 u    -   64    1   60.533  -345760  21.104

Now we need to install GlusterFS on FreeBSD machines – the glusterfs package.

We will add appropriate section to the playbook.

  - name: Install Latest GlusterFS Package
    pkgng:
      state: latest
      name:
      - glusterfs
      - ncdu

You can add more then one package to the pkgng Ansible module – for example I have also added ncdu package.

You can read more about pkgng Ansible module by typing the ansible-doc pkgng command or at least its short version with -s argument.

vbhost % ansible-doc -s pkgng
- name: Package manager for FreeBSD >= 9.0
  pkgng:
      annotation:            # A comma-separated list of keyvalue-pairs of the form `[=]'. A `+' denotes adding
                               an annotation, a `-' denotes removing an annotation, and `:' denotes
                               modifying an annotation. If setting or modifying annotations, a value
                               must be provided.
      autoremove:            # Remove automatically installed packages which are no longer needed.
      cached:                # Use local package base instead of fetching an updated one.
      chroot:                # Pkg will chroot in the specified environment. Can not be used together with `rootdir' or `jail'
                               options.
      jail:                  # Pkg will execute in the given jail name or id. Can not be used together with `chroot' or `rootdir'
                               options.
      name:                  # (required) Name or list of names of packages to install/remove.
      pkgsite:               # For pkgng versions before 1.1.4, specify packagesite to use for downloading packages. If not
                               specified, use settings from `/usr/local/etc/pkg.conf'. For newer
                               pkgng versions, specify a the name of a repository configured in
                               `/usr/local/etc/pkg/repos'.
      rootdir:               # For pkgng versions 1.5 and later, pkg will install all packages within the specified root directory.
                               Can not be used together with `chroot' or `jail' options.
      state:                 # State of the package. Note: "latest" added in 2.7

You can read more about this particular module on the following – https://docs.ansible.com/ansible/latest/modules/pkgng_module.html – Ansible page.

We will now add GlusterFS nodes to the /etc/hosts file and add autoboot_delay=1 parameter to the /boot/loader.conf file so our systems will boot 9 seconds faster as 10 is the default delay setting.

Here is out gluster-setup.yml Ansible playbook this far.

vbhost % cat gluster-setup.yml
---
- name: Install and Setup GlusterFS on FreeBSD
  hosts: gluster
  user: root
  tasks:

  - name: Install Latest Python Package
    pkgng:
      name: python
      state: latest

  - name: Enable NTPD Service
    raw: sysrc ntpd_enable=YES

  - name: Start NTPD Service
    service:
      name: ntpd
      state: started

  - name: Install Latest GlusterFS Package
    pkgng:
      state: latest
      name:
      - glusterfs
      - ncdu

  - name: Add Nodes to /etc/hosts File
    blockinfile:
      path: /etc/hosts
      block: |
        10.0.10.11 gluster1
        10.0.10.12 gluster2
        10.0.10.13 gluster3
        10.0.10.14 gluster4
        10.0.10.15 gluster5
        10.0.10.16 gluster6

  - name: Add autoboot_delay to /boot/loader.conf File
    lineinfile:
      path: /boot/loader.conf
      line: autoboot_delay=1
      create: yes

Here is the result of the execution of this playbook.

vbhost % ansible-playbook -i hosts gluster-setup.yml

PLAY [Install and Setup GlusterFS on FreeBSD] **********************************

TASK [Gathering Facts] *********************************************************
ok: [gluster3]
ok: [gluster5]
ok: [gluster1]
ok: [gluster4]
ok: [gluster2]
ok: [gluster6]

TASK [Install Latest Python Package] *******************************************
ok: [gluster4]
ok: [gluster2]
ok: [gluster5]
ok: [gluster3]
ok: [gluster1]
ok: [gluster6]

TASK [Install Latest GlusterFS Package] ****************************************
ok: [gluster2]
ok: [gluster1]
ok: [gluster3]
ok: [gluster5]
ok: [gluster4]
ok: [gluster6]

TASK [Add Nodes to /etc/hosts File] ********************************************
changed: [gluster5]
changed: [gluster4]
changed: [gluster2]
changed: [gluster3]
changed: [gluster1]
changed: [gluster6]

TASK [Enable GlusterFS Service] ************************************************
changed: [gluster1]
changed: [gluster4]
changed: [gluster2]
changed: [gluster3]
changed: [gluster5]
changed: [gluster6]

TASK [Add autoboot_delay to /boot/loader.conf File] ****************************
changed: [gluster3]
changed: [gluster2]
changed: [gluster5]
changed: [gluster1]
changed: [gluster4]
changed: [gluster6]

PLAY RECAP *********************************************************************
gluster1                   : ok=6    changed=3    unreachable=0    failed=0
gluster2                   : ok=6    changed=3    unreachable=0    failed=0
gluster3                   : ok=6    changed=3    unreachable=0    failed=0
gluster4                   : ok=6    changed=3    unreachable=0    failed=0
gluster5                   : ok=6    changed=3    unreachable=0    failed=0
gluster6                   : ok=6    changed=3    unreachable=0    failed=0

Let’s check that FreeBSD machines can now ping each other by names.

vbhost % ssh gluster6 cat /etc/hosts
# LOOPBACK
127.0.0.1      localhost localhost.my.domain
::1            localhost localhost.my.domain

# BEGIN ANSIBLE MANAGED BLOCK
10.0.10.11 gluster1
10.0.10.12 gluster2
10.0.10.13 gluster3
10.0.10.14 gluster4
10.0.10.15 gluster5
10.0.10.16 gluster6
# END ANSIBLE MANAGED BLOCK

vbhost % ssh gluster1 ping -c 1 gluster3
PING gluster3 (10.0.10.13): 56 data bytes
64 bytes from 10.0.10.13: icmp_seq=0 ttl=64 time=1.924 ms

--- gluster3 ping statistics ---
1 packets transmitted, 1 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 1.924/1.924/1.924/0.000 ms

… and our /boot/loader.conf file.

vbhost % ssh gluster4 cat /boot/loader.conf
autoboot_delay=1

Now we need to create directories for GlusterFS data. Without better idea we will use /data directory with /data/colume1 as the directory for volume1 and bricks will be put as /data/volume1/brick1 dirs. In this setup I will use just one brick per server but in production environment you would probably use one brick per physical disk.

Here is the playbook command we will use to create these directories on FreeBSD machines.

  - name: Create brick* Directories for volume1
    raw: mkdir -p /data/volume1/brick` hostname | grep -o -E '[0-9]+' `

After executing it with ansible-playbook -i hosts gluster-setup.yml command the directories has beed created.

vbhost % ssh gluster2 find /data -ls | column -t
2247168  8  drwxr-xr-x  3  root  wheel  512  Dec  28  17:48  /data
2247169  8  drwxr-xr-x  3  root  wheel  512  Dec  28  17:48  /data/volume2
2247170  8  drwxr-xr-x  2  root  wheel  512  Dec  28  17:48  /data/volume2/brick2


We now need to add glusterd_enable=YES to the /etc/rc.conf file on GlusterFS nodes and then start the GlsuterFS service.

This is the snippet we will add to our playbook.

  - name: Enable GlusterFS Service
    raw: sysrc glusterd_enable=YES

  - name: Start GlusterFS Service
    service:
      name: glusterd
      state: started

Let’s make quick random verification.

vbhost % ssh gluster4 service glusterd status
glusterd is running as pid 2684.

Now we need to proceed to the last part of the GlusterFS setup – create the volume.

We will do this from the gluster1 – the 1st node of the GlusterFS cluster.

First we need to peer probe other nodes.

gluster1 # gluster peer probe gluster1
peer probe: success. Probe on localhost not needed
gluster1 # gluster peer probe gluster2
peer probe: success.
gluster1 # gluster peer probe gluster3
peer probe: success.
gluster1 # gluster peer probe gluster4
peer probe: success.
gluster1 # gluster peer probe gluster5
peer probe: success.
gluster1 # gluster peer probe gluster6
peer probe: success.

Then we can create the volume. We will need to use force option to because for our example setup we will use directories on the root partition.

gluster1 # gluster volume create volume1 \
             disperse-data 4 \
             redundancy 2 \
             transport tcp \
             gluster1:/data/volume1/brick1 \
             gluster2:/data/volume1/brick2 \
             gluster3:/data/volume1/brick3 \
             gluster4:/data/volume1/brick4 \
             gluster5:/data/volume1/brick5 \
             gluster6:/data/volume1/brick6 \
             force
volume create: volume1: success: please start the volume to access data

We can now start the volume1 GlsuerFS volume.

gluster1 # gluster volume start volume1
volume start: volume1: success

gluster1 # gluster volume status volume1
Status of volume: volume1
Gluster process                             TCP Port  RDMA Port  Online  Pid
------------------------------------------------------------------------------
Brick gluster1:/data/volume1/brick1         N/A       N/A        N       N/A
Brick gluster2:/data/volume1/brick2         N/A       N/A        N       N/A
Brick gluster3:/data/volume1/brick3         N/A       N/A        N       N/A
Brick gluster4:/data/volume1/brick4         N/A       N/A        N       N/A
Brick gluster5:/data/volume1/brick5         N/A       N/A        N       N/A
Brick gluster6:/data/volume1/brick6         N/A       N/A        N       N/A
Self-heal Daemon on localhost               N/A       N/A        N       644
Self-heal Daemon on gluster6                N/A       N/A        N       643
Self-heal Daemon on gluster5                N/A       N/A        N       647
Self-heal Daemon on gluster2                N/A       N/A        N       645
Self-heal Daemon on gluster3                N/A       N/A        N       645
Self-heal Daemon on gluster4                N/A       N/A        N       645

Task Status of Volume volume1
------------------------------------------------------------------------------
There are no active volume tasks

gluster1 # gluster volume info volume1

Volume Name: volume1
Type: Disperse
Volume ID: 68cf9607-16bc-4550-9b6b-16a5c7656f51
Status: Started
Snapshot Count: 0
Number of Bricks: 1 x (4 + 2) = 6
Transport-type: tcp
Bricks:
Brick1: gluster1:/data/volume1/brick1
Brick2: gluster2:/data/volume1/brick2
Brick3: gluster3:/data/volume1/brick3
Brick4: gluster4:/data/volume1/brick4
Brick5: gluster5:/data/volume1/brick5
Brick6: gluster6:/data/volume1/brick6
Options Reconfigured:
nfs.disable: on
transport.address-family: inet

Here are contents of currently unused/empty brick.

gluster1 # find /data/volume1/brick1
/data/volume1/brick1
/data/volume1/brick1/.glusterfs
/data/volume1/brick1/.glusterfs/indices
/data/volume1/brick1/.glusterfs/indices/xattrop
/data/volume1/brick1/.glusterfs/indices/entry-changes
/data/volume1/brick1/.glusterfs/quarantine
/data/volume1/brick1/.glusterfs/quarantine/stub-00000000-0000-0000-0000-000000000008
/data/volume1/brick1/.glusterfs/changelogs
/data/volume1/brick1/.glusterfs/changelogs/htime
/data/volume1/brick1/.glusterfs/changelogs/csnap
/data/volume1/brick1/.glusterfs/brick1.db
/data/volume1/brick1/.glusterfs/brick1.db-wal
/data/volume1/brick1/.glusterfs/brick1.db-shm
/data/volume1/brick1/.glusterfs/00
/data/volume1/brick1/.glusterfs/00/00
/data/volume1/brick1/.glusterfs/00/00/00000000-0000-0000-0000-000000000001
/data/volume1/brick1/.glusterfs/landfill
/data/volume1/brick1/.glusterfs/unlink
/data/volume1/brick1/.glusterfs/health_check

The 6-node GlusterFS cluster is now complete and volume1 available to use.

Alternative

The GlusterFS’s documentation Quick Start Guide also suggests using Ansible to deploy and manage GlusterFS with gluster-ansible repository or gluster-ansible-cluster but they have below requirements.

  • Ansible version 2.5 or above.
  • GlusterFS version 3.2 or above.

As GlusterFS on FreeBSD is at 3.11.1 version I did not used them.

FreeBSD Client

We will now use another VirtualBox machine – also based on the same FreeBSD 12.0-RELEASE image – to create FreeBSD Client machine that will mount our volume1 volume.

We will need to install glusterfs package with pkg(8) command. Then we will use mount_glusterfs command to mount the volume. Keep in mind that in order to mount GlusterFS volume the FUSE (fuse.ko kernel module is needed.

client # pkg install glusterfs

client # kldload fuse

client # mount_glusterfs 10.0.10.11:volume1 /mnt

client # echo $?
0

client # mount
/dev/gpt/rootfs on / (ufs, local, soft-updates)
devfs on /dev (devfs, local, multilabel)
/dev/fuse on /mnt (fusefs, local, synchronous)

client # ls /mnt
ls: /mnt: Socket is not connected

It is mounted but does not work. The solution to this problem is to add appropriate /etc/hosts entries to the GlusterFS nodes.

client # cat /etc/hosts
::1                     localhost localhost.my.domain
127.0.0.1               localhost localhost.my.domain

10.0.10.11 gluster1
10.0.10.12 gluster2
10.0.10.13 gluster3
10.0.10.14 gluster4
10.0.10.15 gluster5
10.0.10.16 gluster6

Lets mount it again now with needed /etc/hosts entries.

client # umount /mnt

client # mount_glusterfs gluster1:volume1 /mnt

client # ls /mnt
client #

We now have our GlusterFS volume properly mounted and working on the FreeBSD Client machine.

Lets write some file there with dd(8) to see how it works.

client # dd  FILE bs=1m count=100 status=progress
  73400320 bytes (73 MB, 70 MiB) transferred 1.016s, 72 MB/s
100+0 records in
100+0 records out
104857600 bytes transferred in 1.565618 secs (66975227 bytes/sec)

Let’s see how it looks in the brick directory.

gluster1 # ls -lh /data/volume1/brick1
total 25640
drw-------  10 root  wheel   512B Jan  3 18:31 .glusterfs
-rw-r--r--   2 root  wheel    25M Jan  3 18:31 FILE

gluster1 # find /data
/data/
/data/volume1
/data/volume1/brick1
/data/volume1/brick1/.glusterfs
/data/volume1/brick1/.glusterfs/indices
/data/volume1/brick1/.glusterfs/indices/xattrop
/data/volume1/brick1/.glusterfs/indices/xattrop/xattrop-aed814f1-0eb0-46a1-b569-aeddf5048e06
/data/volume1/brick1/.glusterfs/indices/entry-changes
/data/volume1/brick1/.glusterfs/quarantine
/data/volume1/brick1/.glusterfs/quarantine/stub-00000000-0000-0000-0000-000000000008
/data/volume1/brick1/.glusterfs/changelogs
/data/volume1/brick1/.glusterfs/changelogs/htime
/data/volume1/brick1/.glusterfs/changelogs/csnap
/data/volume1/brick1/.glusterfs/brick1.db
/data/volume1/brick1/.glusterfs/brick1.db-wal
/data/volume1/brick1/.glusterfs/brick1.db-shm
/data/volume1/brick1/.glusterfs/00
/data/volume1/brick1/.glusterfs/00/00
/data/volume1/brick1/.glusterfs/00/00/00000000-0000-0000-0000-000000000001
/data/volume1/brick1/.glusterfs/landfill
/data/volume1/brick1/.glusterfs/unlink
/data/volume1/brick1/.glusterfs/health_check
/data/volume1/brick1/.glusterfs/ac
/data/volume1/brick1/.glusterfs/ac/b4
/data/volume1/brick1/.glusterfs/11
/data/volume1/brick1/.glusterfs/11/50
/data/volume1/brick1/.glusterfs/11/50/115043ca-420f-48b5-af05-c9552db2e585
/data/volume1/brick1/FILE

Linux Client

I will also show how to mount GlusterFS volume on the Red Hat clone CentOS in its latest 7.6 incarnation. It will require glusterfs-fuse package installation.

[root@localhost ~]# yum install glusterfs-fuse


[root@localhost ~]# rpm -q --filesbypkg glusterfs-fuse | grep /sbin/mount.glusterfs
glusterfs-fuse            /sbin/mount.glusterfs

[root@localhost ~]# mount.glusterfs 10.0.10.11:volume1 /mnt
Mount failed. Please check the log file for more details.

Similarly like with FreeBSD Client the /etc/hosts entries are needed.

[root@localhost ~]# cat /etc/hosts
127.0.0.1   localhost localhost.localdomain localhost4 localhost4.localdomain4
::1         localhost localhost.localdomain localhost6 localhost6.localdomain6

10.0.10.11 gluster1
10.0.10.12 gluster2
10.0.10.13 gluster3
10.0.10.14 gluster4
10.0.10.15 gluster5
10.0.10.16 gluster6

[root@localhost ~]# mount.glusterfs 10.0.10.11:volume1 /mnt

[root@localhost ~]# ls /mnt
FILE

[root@localhost ~]# mount
10.0.10.11:volume1 on /mnt type fuse.glusterfs (rw,relatime,user_id=0,group_id=0,default_permissions,allow_other,max_read=131072)

With apropriate /etc/hosts entries it works as desired. We see the FILE file generated fron the FreeBSD Client machine.

GlusterFS Cluster Redundancy

After messing with the volume and creating and deleting various files I also tested its redundancy. In theory this RAID6 equivalent protection should protect us from the loss of two of six servers. After shutdown of two VirtualBox machines the volume is still available and ready to use.

Closing Thougts

Pity that FreeBSD does not provide more modern GlusterFS package as currently only 3.11.1 version is available.

EOF

Ghost in the Shell – Part 3

Time to bring some life into the Ghost in the Shell series with Part 3 article.

You may want to check other articles in the Ghost in the Shell series on the Ghost in the Shell – Global Page where you will find links to all episodes of the series along with table of contents for each episode’s contents.

Query Functions

I haven’t found better name for that solution. There are generally two types of UNIX people. These that prefer to navigate and operate with basic ls/cd/mv/mkdir/rm commands and those who use some file manager like Midnight Commander (mc) or ranger or vifm or … you get the idea. I have tried various CLI file managers but always came back to navigate without them. If you are one of those people then these Query Functions are for you πŸ™‚

The so called Query Functions are for filter the information you look for. For example if you have directory with large number of files, then you would probably do something like that.

% ls | grep QUERY

… or if you also want to include subdirectories then something like that.

% find . | grep QUERY

For both of these examples you would also probably want to sometimes search case sensitive or insensitive depending on the need.

That leads us to four Query Functions:

  • q is an equivalent of ls | grep -i QUERY command.
  • Q is an equivalent of ls | grep QUERY command.
  • qq is an equivalent of find . | grep -i QUERY command.
  • QQ is an equivalent of find . | grep QUERY command.

Thus if I need to query the contents of directory while searching for something is very fast with q SOMETHING.

These are definitions of these Query Functions:

# SHORT QUERY FUNCTIONS q()
  q() {
    if [ ${#} -eq 1 ]
    then
      ls | grep --color -i ${1} 2> /dev/null
    else
      echo "usage: q string"
    fi
  }
     
# SHORT QUERY FUNCTIONS Q()
  Q() {
    if [ ${#} -eq 1 ]
    then
      ls | grep --color ${1} 2> /dev/null
    else
      echo "usage: Q string"
    fi
  }

# SHORT QUERY FUNCTIONS qq()
  qq() {
    if [ ${#} -eq 1 ]
    then
      find . \
        | grep -i ${1} 2> /dev/null \
        | cut -c 3-999 \
        | grep --color -i ${1} 2> /dev/null
    else
      echo "usage: qq string"
    fi
  }

# SHORT QUERY FUNCTIONS QQ()
  QQ() {
    if [ ${#} -eq 1 ]
    then
      find . \
        | grep ${1} 2> /dev/null \
        | cut -c 3-999 \
        | grep ${1} 2> /dev/null
    else
      echo "usage: QQ string"
    fi
  }

The qq and QQ functions uses grep(1) two times to make sure the output is colored.

I assume that You use colored grep(1) described in Ghost in the Shell – Part 2 article.

If you prefer to use alias(1) instead then they would look like that.

# SHORT QUERY FUNCTIONS q() Q() qq() QQ()
  alias q="ls | grep --color -i"
  alias Q="ls | grep --color"
  alias qq="find . | grep -i"
  alias QQ="find . | grep"

The qq and QQ will be little more limited as with functions its possible to trim the output to the exact needs with cut(1).

q.png

qq.png

Lots of people use recursive history search which also helps, but what if you used/typed needed command long ago with the arguments you need now? You would probably search the command with history(1) command and then using grep(1) to limit the results to what you look for. I keep enormous large list of commands to keep in history – with my current setting of 655360 the ~/.zhistory (ZSH) file takes about 2.7 MB size. I also wanted to be sure that two identical commands would not be kept in history hence the setopt hist_ignore_all_dups ZSH option enabled. When I wc -l my ~/.zhistory file it currently has 75695 lines of commands.

% grep HISTSIZE /usr/local/etc/zshrc
export HISTSIZE=655360
export SAVEHIST=${HISTSIZE}

% grep dups /usr/local/etc/zshrc
setopt hist_ignore_all_dups

Now back to Query Functions for history:

  • h is an equivalent of cat ~/.zhistory | grep -i QUERY command.
  • H is an equivalent of cat ~/.zhistory | grep QUERY command.

They fit in aliases this time. In alias(1) we will use just grep(1) to not ‘do’ Useless Use of Cat.

Here are the Query Functions for history.

# SHORT HISTORY ALIASES h() H()
  alias h='< ~/.zhistory grep -i'
  alias H='< ~/.zhistory grep'

h

… but what if we would like to filter the outputs of q family and h family Query Functions? The obvious response is using grep(1) like q QUERY | grep ANOTHER or h QUERY | grep ANOTHER for example. To make that faster we will make g and G shortcuts.

  • g is an equivalent of grep -i command.
  • G is an equivalent of just grep command.

Here they are.

# SHORT GREP FUNCTIONS g() G()
  alias g='grep -i'
  alias G='grep'

Now it will be just q QUERY | g ANOTHER and h QUERY | G ANOTHER for example.

To clear terminal output you may use clear(1) command, some prefer [CTRL]-[L] shortcut but I find ‘c‘ alias to be the fastest solution.

# SHORT GREP FUNCTIONS c()
  alias c='clear'

To make the solution complete I would also add exa(1) here with an alias of ‘e‘.

# SHORT LISTING WITH e()
  alias e='exa --time-style=long-iso --group-directories-first'

Why exa(1) will you ask while there is BSD ls(1) and GNU ls(1) (installed as gls(1) on FreeBSD to not confuse). To add GNU ls(1) to FreeBSD system use the coreutils package.

Well, the BSD ls(1) has two major cons:

  • It is not able to sort directories first.
  • It selects width for ALL columns based on single longest file name.

BSD-ls.png

The BSD ls(1) was used as following alias:

alias ls='ls -p -G -D "%Y.%m.%d %H:%M"'

The GNU ls(1) does not have these two problems but it does color the output only on the very limited pattern like:

  • Not executable file.
  • Executable file.
  • Directory.
  • Link.
  • Device.

GNU-ls.png

The GNU ls(1) was used as following alias:

gls -p -G --color --time-style=long-iso --group-directories-first --quoting-style=literal

Here is where exa(1) comes handy as it does not have any cons like FreeBSD’s ls(1) and it colors a lot more types of files.

e.png

exa --time-style=long-iso --group-directories-first

Its still very simple coloring based on file extension and not magic number as plain (empty) text file SOME-NOT-FILE.pdf is colored like PDF document.

e-pdf.png

But even this ‘limited’ coloring helps in 99% of the cases and while with BSD ls(1) and GNU ls(1) all of these files ‘seem’ like plain text files with exa(1) its obvious from the start which are plain files, which are images and which are ‘documents’ like PDF files for example.

Where Is My Space

On all UNIX and Linux systems there exists du(1) command. Combined with sort(1) it is universal way of searching for space eaters. Example for the / root directory with -g flag to display units in gigabytes.

# cd /
# du -sg * | sort -n
1       bin
1       boot
1       compat
1       COPYRIGHT
1       data
1       dev
1       entropy
1       etc
1       lib
1       libexec
1       media
1       mnt
1       net
1       proc
1       rescue
1       root
1       sbin
1       sys
1       tmp
1       var
2       jail
8       usr
305     home

Contents of UNIX System Resources directory with -m flag to display unit in megabytes.

# cd /usr
# du -sm * | sort -n
1       libdata
1       obj
1       tests
3       libexec
11      sbin
13      include
45      lib32
56      lib
58      share
105     bin
1080    ports
1343    src
5274    local

But its PITA to type cd and du all the time, not to mention that some oldschool UNIX systems does not provide -g or -m flags so on HP-UX you are limited to kilobytes at most.

You may also try -h (human readable) with sort -h (sort human readable) du(1) variant.

# du -smh * | sort -h
512B    data
512B    net
512B    proc
512B    sys
4.5K    COPYRIGHT
4.5K    entropy
5.5K    dev
6.5K    mnt
 53K    media
143K    tmp
205K    libexec
924K    bin
2.2M    etc
3.9M    root
4.6M    sbin
6.2M    rescue
6.6M    lib
 90M    boot
117M    compat
564M    jail
667M    var
5.4G    usr
297G    home

This is where ncdu(1) comes handy. Its ncurses based disk usage analyzer which helps finding that space eaters in very fast time without typing the same commands over and over again. Here is ncdu(1) in action.

First it calculates the sizes of the files.

ncdu.png

After a while you get the output sorted by size.

ncdu-usr.png

If you hit [ENTER] on the directory you will be instantly moved into that directory.

ncdu-usr-local.png

If you delete something with ‘d‘ then remember to recalculate the output with ‘r‘ letter.

It also has great options such as spawning shell ‘b‘ in the current directory or toggle between apparent size and disk usage with ‘a‘ option. The latter is very useful when you use filesystem with builtin compression like ZFS.

       up, k  Move cursor up
     down, j  Move cursor down
 right/enter  Open selected directory
  left, <, h  Open parent directory
           n  Sort by name (ascending/descending)
           s  Sort by size (ascending/descending)
           C  Sort by items (ascending/descending)
           d  Delete selected file or directory
           t  Toggle dirs before files when sorting
           g  Show percentage and/or graph
           a  Toggle between apparent size and disk usage
           c  Toggle display of child item counts
           e  Show/hide hidden or excluded files
           i  Show information about selected item
           r  Recalculate the current directory
           b  Spawn shell in current directory
           q  Quit ncdu

The apparent size using the du(1) command.

Disk usage.

% du -sm books
39145   books

Apparent size.

% du -smA books
44438   books

So I have 1.13 compression ratio on the ZFS filesystem. More then 5 GB saved just in that directory πŸ™‚

Where Are My Files

Once I got some space back I also wanted to know if there are some directories with enormous amount of very small files.

First I came up with my own files-count.sh script solution which is not that long.

#! /bin/sh

export LC_ALL=C

if [ ${#} -eq 0 ]
then
  DIR=.
else
  DIR="${1}"
fi

find "${DIR}" -type d -maxdepth 1 -mindepth 1 \
  | cut -c 3- \
  | while read I
    do
      find "${I}" | wc -l | tr -d '\n'
      echo " ${I}"
    done | sort -n

It works reliably but same as with du | sort tandem you have to retype it (or at least use cd(1) and hit [UP] arrow again) … but then I discovered that ncdu(1) also counts files! It does not provide ‘startup’ argument to start in this count files mode but when you hit ‘c‘ letter it will instantly display count of files in each scanned directory. To sort this output by the count of files hit the ‘C‘ letter (large ‘C‘ letter).

ncdu-files.png

The files-count.sh script still has one advantage over ncdu(1) – the latter stops counting files at 100k which is shown on the screenshot so if You need to search for really big amount of files or just about 100k then files-count.sh script will be more accurate/adequate.

% cd /usr
% files-count.sh 
       1 obj
      36 libdata
     299 sbin
     312 libexec
     390 tests
     498 bin
     723 lib32
     855 lib
    2127 include
   16936 share
  159945 src
  211854 ports
  266021 local

… but what if there were some very big files hidden somewhere deep in the directories tree? The du(1) or ncdu(1) will not help here. As usual I though about short files-big.sh script that will do the job.

#! /bin/sh

export LC_ALL=C

if [ ${#} -eq 0 ]
then
  DIR=.
else
  DIR="${1}"
fi

find "${DIR}" -type f -exec stat -f "%16z; doas rm -f \"%N\"" {} ';' | sort -n

An example usage on the /var directory.

# cd /var
# files-big.sh | tail
        10547304; doas rm -f "./tmp/kdecache-vermaden/icon-cache.kcache"
        29089823; doas rm -f "./db/clamav/clamav-2671b72fce703c2133c61e5bf85aad19.tmp/clamav-373e311ca7f610a39c7cf5c5c5a4fd83.tmp/daily.hdb"
        30138884; doas rm -f "./tmp/pkg-provides-wyK2"
        48271360; doas rm -f "./db/pkg/repo-HardenedBSD.sqlite"
        54816768; doas rm -f "./db/pkg/repo-FreeBSD.sqlite"
        66433024; doas rm -f "./db/pkg/local.sqlite"
        82313216; doas rm -f "./db/clamav/clamav-2671b72fce703c2133c61e5bf85aad19.tmp/clamav-373e311ca7f610a39c7cf5c5c5a4fd83.tmp/daily.hsb"
       117892267; doas rm -f "./db/clamav/main.cvd"
       132431872; doas rm -f "./db/clamav/daily.cld"
       614839082; doas rm -f "./db/pkg/provides/provides.db"

The output is in ‘executable’ format so if you select whole line and paste it into terminal, then this file will be deleted. By default it uses doas(1) but nothing can stop you from putting sudo(8) there. Not sure if you will find it useful but it helped me at least dozen times.

How Many Copies Do You Keep

I often find myself keeping the same files in several places which also wastes space (unless you use ZFS deduplication of course).

The dedup.sh script I once made is little larger so I will not paste it here and just put a link to it.

It has the following options available. You may search/compare files by name or size (fast) or by its MD5 checksum (slow).

% dedup.sh
usage: dedup.sh OPTION DIRECTORY
  OPTIONS: -n   check by name (fast)
           -s   check by size (medium)
           -m   check by md5  (slow)
           -N   same as '-n' but with delete instructions printed
           -S   same as '-s' but with delete instructions printed
           -M   same as '-m' but with delete instructions printed
  EXAMPLE: dedup.sh -s /mnt

Simple usage example.

% cd misc/man
% cp zfs-notes zfs-todo
% dedup.sh -M .
count: 2 | md5: 4ff4be66ab7e5484de2bf7c168ff995a
  doas rm -rf "./zfs-notes"
  doas rm -rf "./zfs-todo"

count: 2 | md5: 6d87f5b1317ea189165fcdc71380735c
  doas rm -rf "./x11"
  doas rm -rf "./xinit"

By copying the zfs-notes file into the zfs-todo file I wanted to show you what dedup.sh will print on the screen, but accidentally I also found another duplicate πŸ™‚

The output of dedup.sh is simple and like with files-big.sh script selecting the while line and pasting it into the terminal will remove the duplicate. By default it uses doas(1) but you can change it into sudo(8) if that works better for you.

Unusual cron(1) Intervals

Most of us already remember what the five fields of crontab(5) file mean, but what if you would like to run command every second … or after reboot only? The answer lies in the man 5 crontab page. Here are these exotic options.

string          meaning
------          -------
@reboot         Run once, at startup of cron.
@yearly         Run once a year, "0 0 1 1 *".
@annually       (same as @yearly)
@monthly        Run once a month, "0 0 1 * *".
@weekly         Run once a week, "0 0 * * 0".
@daily          Run once a day, "0 0 * * *".
@midnight       (same as @daily)
@hourly         Run once an hour, "0 * * * *".
@every_minute   Run once a minute, "*/1 * * * *".
@every_second   Run once a second.

Check cron(1) Environment

Many times I found myself lost lots of time debugging what went wrong when my script was run by the crontab(5) file. Often it was some variable missing or some command or script I used was not in the PATH variable.

To make that debugging faster You can use ENV.sh script to just store the cron(1) environment.

% cat ENV.sh
env > /tmp/ENV.out

The ENV.sh script will write current environment in the /tmp/ENV.out file.

Lets put it into the crontab(5) for a test.

% crontab -l | grep ENV
@every_second ~/ENV.sh

Now after at most a second you can check for the contents of the /tmp/ENV.out file.

% cat /tmp/ENV.out
LOGNAME=vermaden
PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin
PWD=/home/vermaden
HOME=/home/vermaden
USER=vermaden
SHELL=/bin/sh

Now you can easily debug the scripts run by the crontab(5) … at least on the environment part πŸ™‚

Simple HTTP Server

I found myself many times in a situation that I would want to allow download of some files from my machine and SSH could not be used.

This is when python(1) comes handy. It has SimpleHTTPServer (or http.server in Python 3 version) so you can instantly start HTTP server in any directory!

Here are the commands for both Python versions.

  • Python 2.x – python -m SimpleHTTPServer PORT
  • Python 3.x – python -m http.server PORT

I even made a simple http.sh wrapper script to make it even more easy.

#! /bin/sh

if ${#} -ne 1 ]
then
  echo "usage: ${0##*/} PORT"
  exit 1
fi

python -m SimpleHTTPServer ${1}

Example usage.

% cd misc/man
% http.sh 8080
Serving HTTP on 0.0.0.0 port 8080 ...
127.0.0.1 - - [14/Sep/2018 23:06:50] "GET / HTTP/1.1" 200 -
127.0.0.1 - - [14/Sep/2018 23:06:50] code 404, message File not found
127.0.0.1 - - [14/Sep/2018 23:06:50] "GET /favicon.ico HTTP/1.1" 404 -
127.0.0.1 - - [14/Sep/2018 23:09:15] "GET /bhyve HTTP/1.1" 200 -

To stop it simply hit [CTRL]-[C] interrupt sequence.

Here is how it looks in the Epiphany browser.

http.png

Simple FTP Server

Similarly with FTP service, another Python goodie called pyftpdlib (Python FTP Server Library) provides that.

Mine ftp.py wrapper is little bigger as you can write quite comlicated setups with pyftpdlib but mine is simple, it starts in the current directory and adds read only anonymous user and read/write user named writer with WRITER password.

#! /usr/bin/env python

from sys                   import argv,exit
from pyftpdlib.authorizers import DummyAuthorizer
from pyftpdlib.handlers    import FTPHandler
from pyftpdlib.servers     import FTPServer

if len(argv) != 2:
  print "usage:", argv[0], "PORT"
  print
  exit(1)
  
authorizer = DummyAuthorizer()
authorizer.add_user("writer", "WRITER", ".", perm="elradfmw")
authorizer.add_anonymous(".")
handler = FTPHandler
handler.authorizer = authorizer
handler.passive_ports = range(60000, 60001)
address = ("0.0.0.0", argv[1])
ftpd = FTPServer(address, handler)
ftpd.serve_forever()

The ftp.py is handy if you want to enable someone to upload something for you (or you are doing it o the other machine) when SSH/SCP is not possible for some reason.

To stop it simply hit [CTRL]-[C] interrupt sequence.

Here is its terminal startup and logs.

% cd misc/man
% ftp.py 2121
[I 2018-09-14 23:21:53] }}} starting FTP server on 0.0.0.0:2121, pid=64399 {{{
[I 2018-09-14 23:21:53] concurrency model: async
[I 2018-09-14 23:21:53] masquerade (NAT) address: None
[I 2018-09-14 23:21:53] passive ports: 60000->60000

… and how Firefox renders its contents.

ftp.png

Hope you will find some of these useful, see you at Part 4 some day.

UPDATE 1 – More Short Functions

As time flies by I also added several other ‘short functions’ that make my life easier. They are related to mine Universal File Opener named see.sh.
This is the part that I added to mine ~/.zshrc shell config.

# SHORT see.sh OPEN ALIASES
  alias s='see.sh'
  alias o='see-pipe-open.sh'

The additional see-pipe-open.sh helper script is meant to be used in pipes to open all files from stdin.
Example below

% ls
bsd.1.pdf  bsd.2.pdf  bsd.png  unix.1.pdf  unix.2.pdf  NOTES.txt

% q bsd
bsd.1.pdf
bsd.2.pdf
bsd.png

% q bsd | g pdf
bsd.1.pdf
bsd.2.pdf

% q bsd | g pdf | o
// see.sh will open bsd.1.pdf with mupdf(1)
// see.sh will open bsd.2.pdf with mupdf(1)

Now – the q bsd | g pdf | o will open bsd.1.pdf and bsd.2.pdf files according to what is configured in the see.sh handler. In my case it mupdf(1) would be used to open both of them.

As for the other s shortcut – its just faster to type s bsd.1.pdf than see.sh bsd.1.pdf to open a file at terminal πŸ™‚

Regards.

EOF

Ghost in the Shell – Part 2

The article in the Ghost in the Shell series was the first post on my blog, so while I was busy by writing various server related articles and recently the FreeBSD Desktop series its about time for the Part 2 of the Ghost in the Shell series.

You may want to check other articles in the Ghost in the Shell series on the Ghost in the Shell – Global Page where you will find links to all episodes of the series along with table of contents for each episode’s contents.

Lets start with something simple – yet powerful and time saving.

Alias with Arguments

One may of course write any function to do similar job, but keeping track and ‘maintaining’ all those functions becomes complicated and one has to organize itself. This partially applies to aliases, but they are smaller and easier to maintain then whole functions. In any modern shell an alias(1) can also have arguments, while You will not be able to parse them as appropriate as with functions, they do the job for their basic use.

Here is an example of such alias(1) with arguments.

% ls
gfx/ info/ misc/ scripts/ tmp/

% alias lsg='ls | grep'

% lsg gfx
gfx/

Color grep(1) Patterns

As we already ‘touched’ the grep(1) command topic, lets make it more usable by highlighting the found results in color. The ${GREP_COLOR} variable is used for that purpose and it expects a number for a color, here is the table with number-color format.

Color    Number
Black    30
Red      31
Green    32
Yellow   33
Blue     34
Magenta  35
Cyan     36
White    37

You may as well use ‘bold’ output by adding ‘1;‘ before the number, for example.

% echo ${GREP_COLOR}
1;31

You will also have to make an alias(1) to grep(1) with --color argument, like that:

% alias grep='grep --color'

Here is how it looks in practice.

% export GREP_COLOR=31
% alias grep='grep --color'
% dmesg | grep SMP
FreeBSD/SMP: Multiprocessor System Detected: 2 CPUs
FreeBSD/SMP: 1 package(s) x 2 core(s)
SMP: AP CPU #1 Launched!

Here is how it looks on the xterm(1) terminal.

ghost-terminal

Process Management

This one is very useful on any UNIX system, does not matter if its server or desktop.

These are commands and operands that will help us manage processes started by hand:

  • &
  • fg
  • bg
  • jobs
  • kill
  • disown
  • nohup
  • [CTRL]+[Z]
  • [CTRL]+[C]

As you probably already know to start command ‘in the background’ – which means do what I tell you but do not block the terminal – you have to add ‘&‘ (ampersand) at the end of such command. That command does not magically go away and as long as its running its visible by the jobs(1) command. You may use ‘-l‘ switch to also show the PID of background processes.

% galculator &
[1] 8449

% jobs
[1]  + running    galculator

% jobs -l
[1]  + 8449 running    galculator

Now, what of you forget to add ‘&‘ (ampersand) at the end of command but you wanted to put it into the background? Hit [CTRL]+[Z] shortcut (Control key with ‘small’ Z letter) and the process will be put into the suspended state. Now you have several options, you can out that process into the background with bg(1) command – by default it uses last suspended job – %1, you can also bring it back into the foreground blocking the terminal with fg(1) command. You can also list its state with jobs(1) and of course kill(1) it either with PID showed by jobs -l command or by specifying the process number – %1 in that case.

Here is an example.

% galculator
^Z
zsh: suspended  galculator

% jobs
[1]  + suspended  galculator

% bg
[1]  + continued  galculator

% jobs -l
[1]  + 72892 running    galculator

% kill %1
[1]  + terminated  galculator

%

While fg(1) and bg(1) allow you to put command in the background or foreground respectively when the process is in suspended state, one may ask how to ‘switch’ a process to suspended state while its already running in the background. Its done with kill -17 signal called SIGSTOP. You can also bring back such suspended process to running state with kill -19 signal called SIGCONT … or just again use fg(1) or bg(1) command. Other difference between fg(1)/bg(1) commands and more ‘direct’ kill -17/kill -19 commands are that kill(1) does not inform the user what has changed to the process. You may as well use kill -SIGCONT syntax or kill -s SIGCONT if that is more readable for you.

% galculator
^Z
zsh: suspended  galculator

% bg
[1]  + continued  galculator

% xcalc
^Z
zsh: suspended  xcalc

% jobs -l
[1]  - 19537 running    galculator
[2]  + 20563 suspended  xcalc

% kill -17 %1
[1]  + suspended (signal)  galculator

% jobs -l
[1]  + 19537 suspended (signal)  galculator
[2]  - 20563 suspended  xcalc

% kill -SIGCONT %1
% bg %2
[2]  - continued  xcalc

% jobs -l
[1]  + 19537 running    galculator
[2]  - 20563 running    xcalc

Also check man kill and man signal for more information.

What about disown(1) then? Its a ‘magic’ helper when you start some long running jobs directly at the terminal without Screen or Tmux and you need to disconnect that terminal, for example because you are taking your laptop with you. When you do this – depending on the settings of the current shell – the processes in the background may be killed or ‘moved’ to PID 1 (the init(1) of course) as the PPID (Parent PID). To achieve that we will used that disown(1) command. Once you ‘disown’ a process it will no longer be show by the jobs(1) command, but it will run ‘pinned’ to the init(1) process after you disconnect the terminal session.

% galculator
^Z
zsh: suspended  galculator

% bg
[1]  + continued  galculator

% jobs -l
[1]  + 98556 running    galculator

% disown %1

% jobs -l

% pgrep galculator
98556

% pstree -p 98556
─┬◆ 00001 root /sbin/init --
 └─┬─ 48708 vermaden xterm
   └─┬◆ 52463 vermaden -zsh (zsh)
     └──◆ 98556 vermaden galculator

Now its still pinned to the shell in the xterm(1) terminal. After we close the xterm(1) window (or kill that zsh(1) shell) it will switch to init(1) as PPID (Parent PID).

% pstree -p 98556
─┬◆ 00001 root /sbin/init --
 └──◆ 98556 vermaden galculator

% pgrep -P 1 galculator
98556

We are left with nohup(1) then, when and why to use it as we already has great disown(1) magic? Well, disown(1) is not always available, so when You need to put some command into the long background run and disconnect after it its the best possible option. By default the nohup(1) command will log the output of started command into the nohup.out file. Remember that nohup(1) will still run the process in the foreground, to put it into the background use ‘&‘ (ampersand) or [CTRL]+[Z] with bg(1) combo.

% nohup galculator
appending output to nohup.out
^Z
zsh: suspended  nohup galculator

% bg
[1]  + continued  nohup galculator

% jobs -l
[1]  + 22322 running    nohup galculator

% pstree -p 22322
─┬◆ 00001 root /sbin/init --
 └─┬─ 89568 vermaden xterm
   └─┬◆ 91486 vermaden -zsh (zsh)
     └──◆ 22322 vermaden galculator

… and after disconnect out process switched to init(1) as PPID.

% pstree -p 22322
─┬◆ 00001 root /sbin/init --
 └──◆ 22322 vermaden galculator

You may of course end a running process in the foreground with [CTRL]+[C] shortcut, but that is probably already known to you. I just mention it for the ‘completeness’ of the guide.

% galculator
^C

%

Which Which

While the which(1) command shows the full path of the executable found in the first directory of the ${PATH} variable, it also shows what alias is used for that command it there is one. One may ask how then to find information about absolute executable path if it shows and alias(1) instead. Well, you have to use unalias(1) on that command, so which(1) would be showing full path again.

% which caja
caja: aliased to caja --browser --no-desktop

% unalias caja

% which caja
/usr/local/bin/caja

Also be sure to check Smylers comment below about the difference between shell builtin which and /usr/bin/which command.

The difference is that by typing which you are executing your shell builtin command (ZSH in my case) which also takes aliases into account. If you want to omit the unalias part then use /usr/bin/which which will ignore any existing aliases.

% which caja
caja: aliased to caja --browser --no-desktop

% /usr/bin/which caja
/usr/local/bin/caja

Record Session

If you have used PuTTY or MobaXterm in your work, then you appreciate the possibility of saving the terminal output to a file, foe example for the documentation purposes. This is also available ‘natively’ in the shell by using the script(1) command. Remember that script(1) will record also ‘special’ characters like colors, so to properly ‘replay’ the session you may want to either use script(1) or cat(1) commands for that or use less with -R argument.

Here is example recorded script(1) session.

% script script.out
Script started, output file is script.out

% ls
gfx info misc scripts tmp unix.png

% uname -spr
FreeBSD 11.2-RELEASE amd64

% exit
Script done, output file is script.out

% cat script.out
Script started on Sun Jul  8 08:24:06 2018
You have mail.
% ls | grep gfx
gfx
% uname -spr
FreeBSD 11.2-RELEASE amd64
% exit
exit

Script done on Sun Jul  8 08:24:20 2018

% less -R script.out
Script started on Sun Jul  8 08:24:06 2018
You have mail.
% ls | grep gfx
gfx
% uname -spr
FreeBSD 11.2-RELEASE amd64
% exit
exit

Script done on Sun Jul  8 08:24:20 2018

% less script.out
Script started on Sun Jul  8 08:24:06 2018
You have mail.
% ls | grep gfx
ESC[1;31mgfxESC[00mESC[K
% uname -spr
FreeBSD 11.2-RELEASE amd64
% exit
exit

Script done on Sun Jul  8 08:24:20 2018


Edit Command Before Executing

Sometimes you have long multi-line command to execute, so often it is crafted in you favorite ${EDITOR} and then pasted into the terminal. To omit copying and pasting yo may want to check fc(1) command which serves similar purpose. After you type a command, for example simple ls(1) command, and then you type fc(1) command, then fc(1) will take that ls(1) command into your favorite text editor from ${EDITOR} variable, will allow you to edit it and if you save and exit the that editor, it will execute it.

Lets see how it behave by example.

% ls
gfx   books   download   scripts

% fc

Now you are taken into the ${EDITOR} which is vi(1) in my case.

      1 ls
~
~
~
/tmp/zsh999EQ6: unmodified: line 1

Lets made some changes.

      1 ls -l \
      2    -h
~
~
~
~

:wq

After you hit [ENTER] it will exit from ${EDITOR} and execute that command.

total 6181
drwxr-xr-x    87 vermaden  vermaden    87B 2017.12.18 15:30 books/
drwxr-xr-x    12 vermaden  vermaden    12B 2018.06.19 16:02 download/
drwxr-xr-x    19 vermaden  vermaden    20B 2018.05.24 11:52 gfx/
drwx------    12 vermaden  vermaden   310B 2018.07.07 03:23 scripts/

You may show that command by pressing [Up] key to check what has been executed.

% ls -l -h

Edit or Just View

When working in multi-admin environment – especially while debugging – one admin may block other admin’s work by using vi(1) – or just their favorite editor to ‘browse’ the file contents. Good practice in that case is using more(1) or less(1) instead of vi(1), but that frustrates some admins to type vi(1) again if they need to change something.

… and by the way, on FreeBSD more(1) is less(1) πŸ™‚

% uname -spr
FreeBSD 11.2-RELEASE amd64

% ls -i `which less` `which more`
492318 /usr/bin/less  492318 /usr/bin/more

A blocked ‘example’ is shown below when the second admin wanted to browse the /etc/rc.conf file while the first one already did that.

# vim /etc/rc.conf

E325: ATTENTION
Found a swap file by the name "/etc/.rc.conf.swp"
          owned by: root   dated: Sun Jul  8 08:38:35 2018
         file name: /etc/rc.conf
          modified: no
         user name: root   host name: t420s.local
        process ID: 54219 (still running)
While opening file "/etc/rc.conf"
             dated: Fri Jul  6 00:51:11 2018

(1) Another program may be editing the same file.  If this is the case,
    be careful not to end up with two different instances of the same
    file when making changes.  Quit, or continue with caution.
(2) An edit session for this file crashed.
    If this is the case, use ":recover" or "vim -r /etc/rc.conf"
    to recover the changes (see ":help recovery").
    If you did this already, delete the swap file "/etc/.rc.conf.swp"
    to avoid this message.

Swap file "/etc/.rc.conf.swp" already exists!
[O]pen Read-Only, (E)dit anyway, (R)ecover, (Q)uit, (A)bort:

This is where less(1) comes handy because of you open a file in it, you do not ‘block’ access to it and if you need to edit something just hi [V] key (small ‘v’ letter). It will open that file in your ${EDITOR} editor and you can make any changes now.

Reset

Last but not least, often when you paste ‘too much’ into the terminal it becomes ‘fragile’ or ‘broken’. To reset it into the ‘stable’ and ‘proper’ state just use the reset(1) command.

% reset

Hope You find it useful, see you at the Part 3 sometime πŸ˜‰

EOF

Ghost in the Shell – Part 1

I wanted to post this earlier, but the busy daily life does not help πŸ˜‰

This will be first article in the series about efficient working in the shell environment. There are actually a lot articles and blog posts about efficient working in the terminal, but a lot of them are biased towards very specific uses, like hints only for Bash shell or only for specific terminal emulator. For example Moving efficiently in the CLI.

These series are about universal knowledge that would work on most shells and environments. Lets start with hint that I use many times a day that saves a lot time for not having to type …

You may want to check other articles in the Ghost in the Shell series on the Ghost in the Shell – Global Page where you will find links to all episodes of the series along with table of contents for each episode’s contents.

Recall Last Argument of Previous Command

Imagine most simple scenario, creating directory and entering it. Typically its like that:

% mkdir clear-place-for-new-work
% cd clear-place-for-new-work
%

The longer the name, the bigger the chance that You would type mkdir, then hit the [UP] arrow, then [HOME] or [CTRL]+[A] keys and then put cd in the place of mkdir.

With the use of !$ You can recall last argument of the precious command, so it will now look like that.

% mkdir clear-place-for-new-work
% cd !$
cd clear-place-for-new-work
%

Faster isn’t it?

Swap First Occurrence of a Word

The upper example can be used for the next advice as well. By typing ^fromwhat^towhat in the terminal You will swap the first occurrence of word fromwhat word to towhat word in the previous command, lets see how its working.

% mkdir clear-place-for-new-work
% ^mkdir^cd
cd clear-place-for-new-work
%

It still takes more time to write then using the !$ so its useful mostly when there are short things to swap, like numbers, for example ^3^4 to ‘move’ from one target to another. … or also if You can not recall to the last argument of previous command.

There and Back Again

A lot people does not know, that You can go back to previous working directory with dash. Lets assume that You need to get to /tmp directory for one command and get back to where You were to continue the work. Here is an example.

% pwd
/usr/local/etc/bareos/bareos-dir.d/jobdefs
% cd /tmp
% pwd
/tmp% (do needed work in /tmp dir)
% cd -
/usr/local/etc/bareos/bareos-dir.d/jobdefs
% pwd
/usr/local/etc/bareos/bareos-dir.d/jobdefs

You can even create entire directory stack with pushd/popd commands if needed, check Wikipedia article on that for more information. You can also use ${OLDPWD} variable. Useful with umount command for example.

% pwd
/media/backup-pendrive-key
% cd ~
% umount $OLDPWD
% pwd
/home/vermaden

Repeat Command from History

With exclamation mark (!) You can re-invoke the command from history with all its arguments (which sometimes can be risky). For example.

% !pkg
pkg update -f
(runs actual command)
%

Its better to first check what arguments have been used in that command, that is where :p comes handy. Here is its example usage.

% !pkg:p
pkg update -f
(just prints command without running it)
% !pkg
pkg update -f
(runs actual command)
%

Now, as arguments are known its safe to re-invoke the command with arguments. When this can be dangerous? Can ls command can be dangerous, that depends what You have on Your history, check the example below.

% ls | while read I; do rm -f ${I}; done

This command first lists the contents of the current working directory with ls command, then the output is piped to the while loop which invokes rm -f command for each item listed by ls command, which efficiently removes all non-hidden files in current working directory … which probably is not what we mean by typing !ls on the command prompt ;). That is why its valuable to first check what arguments were used with !ls:p syntax.

Enough for now, I will write more parts with more hints on how to efficiently work in the shell/terminal environment.

UPDATE 1

The Ghost in the Shell – Part 1 article was included in the BSD Now 241 – Bowling in the LimeLight episode.

Thanks for mentioning!

UPDATE 2

About Recall last argument of previous command section … there is also $_ that does similar thing as !$ but there is little difference. The !$ is ‘line oriented’ while $_ is ‘previous command oriented’. Below is an example that shows the difference in the behavior.

The $! takes value from last command in ‘previous line’ which means that '-l' value will be used from line 001 and not 'asd' from the current line 002 from previously executed command.

001 % ls -l
002 % echo asd; ls !$ | tail -2
echo asd; ls -l | tail -2
asd
// ls output //

The $_ takes value from last executed command, thus it points at 'asd' used on line 002 and not at '-l' used at previous 001 line.

001 % ls -l
002 % echo asd; ls $_ | tail -2
asd
ls: asd: No such file or directory

On BASH shell there is also [ALT]-[.] shortcut that switches between $! from previous lines. To achieve the same shortcut on ZSH use this line below in ZSH config.

bindkey '\e.' insert-last-word

Thank you Zachery Purnell for pointing that out.

EOF