Wednesday, May 14, 2025

Linux find Command Reference & Cookbook

Using the Linux find command with examples

Looking for a specific command?
Jump to the Cookbook section.


Introduction

Even though I use fzf - fuzzy finder and have integrated it into my shell, I continue to use the traditional find tool. The following document provides an overview of some of the details regarding the use of the Linux find command as well as a cookbook of practical examples demonstrating the use of the command.
The first portion of the document consists of details and explanations from the man pages for find as well as some summaries of the details regarding the usage of the find command.
The second part consists of practical examples of find usages to solve specific search & action requirements.

Name

find - search for files in a directory hierarchy

Description

The find command is used to locate files. find will search any set of directories you specify for files that match the supplied search criteria. find searches the directory tree rooted at each given starting-point by evaluating the given expression.

find can search for files, or directories, by:

  • name
  • owner
  • group
  • type
  • permissions
  • date

find can also be used to execute commands (e.g. grep, mv, rm, etc.) to act upon the query results.


Synopsis

The syntax structure for searching with find looks like this:

find [-H] [-L] [-P] [-D debugopts] [-Olevel] [starting-point...] [expression]

The command has, essentially, three sections which can be briefly summarized as:

find <options> <starting-point> <expression>
  • [options] - The -H, -L and -P options control the treatment of symbolic links. The -D and -O options control diagnostic output and query optimizations, respectively.
  • [starting-point] - List of directories where to search
  • [expression] - Expressions filter the search or perform actions on the files found.

All arguments to find are optional, below are the defaults for each query section:

  • [options] - defaults to . (the current working directory)
  • [starting-point] - defaults to none (select all files)
  • [expressions] (known as the find action) - defaults to ‑print (display the names of found files to standard output).

Technically, the options and actions are all known as find primaries.


Options

The -H, -L and -P options control the treatment of symbolic links. Command-line arguments following these are taken to be names of files or directories to be examined, up to the first argument that begins with -, or the argument ( or !. That argument and any following arguments are taken to be the expression describing what is to be searched for. If no paths are given, the current directory is used. If no expression is given, the expression -print is used.

-H Do not follow symbolic links, except while processing command line arguments.
-L Follow symbolic links.
-P Never follow symbolic links.
-D debugopts Print diagnostic information. For a complete list of valid debug options, see the output of find -D help.

- exec       Show diagnostic information relating to -exec, -execdir, -ok and -okdir
- opt        Show diagnostic information relating to optimisation
- rates      Indicate how often each predicate succeeded
- search     Navigate the directory tree verbosely
- stat       Trace calls to stat(2) and lstat(2)
- time       Show diagnostic information relating to time-of-day and timestamp comparisons
- tree       Display the expression tree
- all        Set all of the debug flags (but help)
- help       Explain the various -D options  

Olevel Enables query optimization. The find program reorders tests to speed up execution while preserving the overall effect.

For example:

find -D exec -name test.txt -type f -execdir mv {} example.txt \;

Results:

DebugExec: launching process (argc=3): ‘mv’ ‘./test.txt’ ‘example.txt’
DebugExec: process (PID=25192) terminated with exit status: 0

As with all items outlined here, refer to man find for additional, specific details.


Expressions

The part of the command line after the list of starting points is the expression. This is a kind of query specification describing how we match files and what we do with the files that were matched. An expression is composed of a sequence of things:

  • Test expressions
    • Tests return a true or false value, usually on the basis of some property of a file we are considering. The -empty test for example is true only when the current file is empty.
  • Action expressions
    • Actions have side effects (such as printing something on the standard output) and return either true or false, usually based on whether or not they are successful. The -print action for example prints the name of the current file on the standard output.
  • Global options
    • Global options affect the operation of tests and actions specified on any part of the command line. Global options always return true. The -depth option for example makes find traverse the file system in a depth-first order.
  • Positional options
    • Positional options affect only tests or actions which follow them. Positional options always return true. The -regextype option for example is positional, specifying the regular expression dialect for regular expressions occurring later on the command line.
  • Operators
    • Operators join together the other items within the expression. They include for example -o (meaning logical OR) and -a (meaning logical AND). Where an operator is missing, -a is assumed.

When using multiple expressions without specifying any operator, the AND operator is implicitly used.

Example Directory Structure

For the next two sections(test expressions and action expressions), the examples will be using the directory structure below. If you want to follow along with the examples, re-create this on your local machine.

        .testdir        # (root/current working directory)  
        ├── example.txt  
        ├── image.jpg  
        ├── topdir1  
        │   ├── dir_a  
        │   │   └── image.jpg  
        │   ├── dir_b  
        │   │   ├── example.txt  
        │   │   └── image.jpg  
        │   ├── dir_c  
        │   └── image.jpg  
        ├── topdir2  
        │   ├── dir_a  
        │   │   └── image.jpg  
        │   ├── dir_b  
        │   ├── dir_c  
        │   │   └── image.jpg  
        │   ├── myfile.txt  
        │   └── myfile1.txt  
        └── topdir3  
            ├── dir_a  
            │   └── image.jpg  
            ├── dir_b  
            │   └── MyFile.txt  
            └── image.jpg  

Note: the folder.jpg & myfile1.txt should not be empty files. Use an actual file containing an image/text of any size.

Here is a basic example that breaks down a simple find command example into its respective elements:

find topdir2 -name "myfile.txt" -perm 644
  • topdir2 - the starting point of the search.
  • -name - A test expression.
  • "myfile.txt" - Value of the expression -name.
  • -perm - Another test expression.
  • 644 - The value of the expression -perm.


Test Expressions

Test expressions are used to filter the folders and files.

Filtering Empty File

The -empty option will search only empty files and directories. It does not need a value.

find . -empty -type f

Filtering by File Name

The expression -name <value> filter files and directories by file name. Regular expressions are not allowed for the <value>, but shell patterns (also called glob operators) are permitted, such as *, ?, or [].

find . -name '*.txt' find . -name 'image.jpg'

Filtering by File Path

The expression -path <value> filters files and directories by their file paths. Like -name, it does not accept regular expressions but shell patterns.

find . -path '**/topdir1/*.jpg'

Filtering Using a Regex

File name matches regular expression pattern using -regex <value>. This is a match on the whole path, not a search. For example, to match a file named ./foobar, you can use the regular expression .*bar. or .*b.*3, but not f.*r3

find . -regex '.*1.txt'

The positional option -regextype can be used before -regex, to specify the regex engine you want to use. To output a list of regex engines supported, run find . -regextype dummy. Example output:

find: Unknown regular expression type ‘dummy’; valid types are ‘findutils-default’, ‘ed’, ‘emacs’, ‘gnu-awk’, ‘grep’, ‘posix-awk’, ‘awk’, ‘posix-basic’, ‘posix-egrep’, ‘egrep’, ‘posix-extended’, ‘posix-minimal-basic’, ‘sed’.

The following example will find every txt and jpg file using egrep, the extended regular expression engine(ERE):

find . -regextype "egrep" -regex '.*(txt|jpg)$'

Case-insensitive searches can be performed by adding the prefix i to the above mentioned expressions. For example: -iname, -ipath, or -iregex.

Filtering by Type of File

The most common file types to filter on are:

  • f - File
  • d - Directory
  • l - Symbolic link

To search for more than one type at once, separate the options by a comma ,.

find . -name 'dir_a' -type d

Filtering by Permissions

Files can be filtered by whether they are -executable, -readable, or writeable for the current user. For additional granularity, you can use -perm <value>, where <value> can be:

  • A string beginning with / and followed by a series of rules using the OR Boolean operator. For example, -perm /u=w,g=e (writable by the owner, and executable by the group).
  • A string beginning with - and followed by a series of rules using the AND Boolean operator. For example, -perm -u=w,g=e (writable by owner, or executable by the group).
  • An octal number, for example: 644.

Filtering by Owner or Group

  • -user <value> where <value> is a username.
  • -group <value> where <value> is a groupname.


Action Expressions


Deleting Files

Deleting files and directories can be accomplished using the -delete option. The following command will delete all files and directories when their names begin with test.

find . -name "test*" -delete

WARNING: This will permanently delete your files. Use with caution, if at all.

Running a Command on Each Result

The -exec expression executes a command. The string {} is replaced by the current file name being processed everywhere it occurs in the arguments to the command. All following arguments are taken to be arguments to the command until an argument consisting of ; is encountered. Both of these constructions may need to be escaped with a backslash or quoted to protect them from expansion in the shell.

Running a Command in Working Directory

  • find . -exec basename '{} ';' - Run the command basename for every result of the search.
  • find . -exec bash -c 'basename "${0%.*}"' '{}' \; - The command bash -c will allow us to expand parameters. ${0%.*} is used here to remove the file extension from each result.
  • find . -name 'image.jpg' -exec file {} \; - Run the file command against all .jpg files returned from the search.

You can also use the expression -ok. It is the same as the -exec option, except that find will prompt you, asking if you really want to run the command. This confirmation will be asked for each result.

find . -name "image.jpg" -ok file {} \;

Running a Command in Starting Directory

The two expressions -execdir and -okdir work like -exec and -ok respectively, except that the commands won’t run in your current working directory, but in the starting directory (the first argument of find).

find topdir1/dir_b -exec bash -c 'basename "${0%*.}"' '{}' \;

Rename every .jpg file in the topdir1 directory with _old and keep same extension:

find topdir1 -name "image.jpg" -type f -execdir rename 's/\.jpg$/_old.jpg/' {} \;

Changing the Output

The following options will change the output of the search results:

  • -print - This is the default action even when not specified. It simply prints every result.
  • -ls - Works like the regular ls command.
  • -print0 - By default, the separator between different results is a \n newline character. With this option, the separator is a null character. Useful if you want to pipe results to xargs -0.
  • -printf - Output files with the information you need. For example: find . -printf %d %p will print the depth of the file in the file tree (%d) and the file name (%p).

Writing the Output to a File

You can also use a bunch of action expressions to write find’s output to a file. You just need to prefix the expression we saw above with a f. For example: -fls, -fprint, -fprint0 or -fprintf. The value of these expressions will be the file written.

Operators

When no operators are explicitly specified, the -and operator is used implicitly between each expression.

  • ! - Negate the expression following it.
  • -or or -o - Logical OR.
  • -and or -a - Logical AND.
  • , - Adding a comma is useful to use different sets of expressions while traversing the filesystem once.

    See the Examples section, below, for several use cases with operators.


Cookbook

The following examples make up the find command cookbook.

Find command structure

Here is a basic example that decomposes a simple find command construct and its respective elements:

find topdir2 -name "myfile.txt" -perm 644
  • topdir2 - the starting point of the search.
  • -name - A test expression.
  • "myfile.txt" - Value of the expression -name.
  • -perm - Another test expression.
  • 644 - The value of the expression -perm.

Basic find file commands

find / -name foo.txt -type f -print # full command
find / -name foo.txt -type f # -print isn't necessary
find / -name foo.txt # don't have to specify "type==file"
find . -name foo.txt # search under the current dir
find . -name "foo.*" # wildcard
find . -name "*.txt" # wildcard
find /users/al -name Cookbook -type d # search '/users/al' dir

find . -iname foo                 # find foo, Foo, FOo, FOO, etc.  
find . -iname foo -type d                    # same thing, but only dirs  
find . -iname foo -type f                    # same thing, but only files  

Search multiple directories

find /opt /usr /var -name foo.scala -type f

Files with different extensions

find . -type f \( -name "*.c" -o -name "*.sh" \) # "*.c" and "*.sh" files find . -type f \( -name "*cache" -o -name "*xml" -o -name "*html" \) # three patterns

Files that don't match a pattern (-not)

find . -type f -not -name "*.html" # find all files not ending in ".html"

Files by text in the file (find + grep)

find . -type f -name "*.java" -exec grep -l StringBuffer {} \; # find StringBuffer in all *.java files

find . -type f -name "*.java" -exec grep -il string {} \; # ignore case with -i option

find . -type f -name "*.gz" -exec zgrep 'GET /foo' {} \; # search for a string in gzip'd files

5 lines before, 10 lines after grep matches

find . -type f -name "*.scala" -exec grep -B5 -A10 'null' {} \;

Files and act on them (find + exec)

find /usr/local -name "*.html" -type f -exec chmod 644 {} \; # change files to mode 644
find htdocs cgi-bin -name "*.cgi" -type f -exec chmod 755 {} \; # change files to mode 755
find . -name "*.pl" -exec ls -ld {} \; # run ls command on files found
find . -type f -iname ".python-version" -print -exec sed -i 's/3.10.5/3.10.6/g' {} \;

Find and replace(sed)

find templates -name "*.html" -type f -exec sed -i 's/javascript:/#\"\ onclick=\"/g' {} \;

Find and copy

find . -type f -name "*.mp3" -exec cp {} ~/tmp/ \; # cp files to ~/tmp/

Copy one file to many directories

find dir1 dir2 dir3 dir4 -type d -exec cp header.shtml {} \;    # copy file  to dirs

Find and delete

find . -type f -name "Foo*" -exec rm {} \;   # remove "Foo*" files under current dir
find . -type d -name CVS -exec rm -r {} \;   # remove subdirectories named "CVS" under current dir
find . -name ".mediaartlocal" -type d -exec rm -r '{}' \; 

Files by modification time

sudo find / -type f -mmin -10
find . -mtime 1               # 24 hours
find . -mtime -7              # last 7 days
find . -mtime -7 -type f      # just files
find . -mtime -7 -type d      # just dirs

By modification time using a temp file

touch 09301330 foo   # 1) create a temp file with a specific timestamp
find . -mnewer foo   # 2) returns a list of new files
rm foo               # 3) rm the temp file

find and tar

find . -type f -name "*.java" | xargs tar cvf myfile.tar
find . -type f -name "*.java" | xargs tar rvf myfile.tar

find, tar, and xargs

find . -name -type f '*.mp3' -mtime -180 -print0 | xargs -0 tar rvf music.tar

 (-print0 helps handle spaces in filenames)

Rename

find . -name "folder.jpg" -type f -execdir mv {} cover.jpg \;
find ~ -iname "*new*" -exec mv -v '{}' /media/current-projects/ \;
find . -type f -name "*.wiki" -exec rename 's/\.wiki$/.md/' '{}' \;


Further Reading

Most the information & examples provided above were originally sourced from the following resources:

Tuesday, May 13, 2025

Adding GitHub labels using the gh CLI

A short guide and template for GitHub issue labels

Basic notes and template for creating labels for GitHub issues using the gh command line tool.

Jump directly to the new label template.

Introduction

This post is mostly about one command. Specifically, it's about one sub-command available with the gh CLI tool: gh label create. It's not because I think that it's a particularly interesting or complicated command. It's mostly because I rarely use the command infrequently and I need a reference for the "standard" that I have casually set up for myself. I work with git and gh commands daily. Many have been successfully committed to muscle memory. With other lengthy gh commands, I've turned a few of them into memorable aliases. I should probably make use of gh alias set; but, that's for another day.

As stated in GitHub's documentation on managing labels:

You can manage your work on GitHub by creating labels to categorize issues, pull requests, and discussions. You can apply labels in the repository the label was created in. Once a label exists, you can use the label on any issue, pull request, or discussion within that repository.

I only use the gh label create command when I create a new repository. The time between creating a new project varies from anywhere between a couple of weeks and several months. So, naturally, I tend to forget commands that haven't entered my muscle memory. With my projects using Python web frameworks, I've mostly standardized on my issue labels and can use the gh label clone command for these projects.

The GitHub organizations feature could also help facilitate the management of default labels across repositories. I, personally, have no plans on using it for now; but, it may be of use for some.

gh CLI label syntax

From the gh CLI manual:

  • USAGE

    • gh label [flags]
  • AVAILABLE COMMANDS

    • clone: Clones labels from one repository to another
    • create: Create a new label
    • delete: Delete a label from a repository
    • edit: Edit a label
    • list: List labels in a repository
  • FLAGS

    • -R, --repo [HOST/]OWNER/REPO Select another repository using the [HOST/]OWNER/REPO format

gh label create command format

gh label create <name> [flags]
gh label create <name> -d "description_string" -c "color_string"

Interlude: Color Selection

According to the gh manual, the label color needs to be specified as a six character hex value. Despite my working in desktop publishing and pre-press in a prior life, I don't have many hex color values memorized; so, I tend to rely on a color chart or a color picker application to help me in my color decisions. The availability of color charts and hex color codes on the internet is abundant. Plentiful even. There must be at least 10 color charts available online for reference. Here are two that I use frequently:

I'm also partial to using a locally installed color picker application. I've been using gcolor3 lately. It's simple and meets my basic needs. For alternatives, here is an OK list of other color picker applications available on Linux distros for installation:

Default Issue Labels

Label Name Description Color
bug Something isn't working #d73a4a
documentation Improvements/additions to documentation #0075ca
duplicate This issue or pull request already exists #cfd3d7
enhancement New feature of request #a2eeef
help wanted Extra attention is needed #008672
good first issue Good for newcomers #7057ff
invalid This doesn't seem right #e4e669
question Further information is requested #d876e3
wontfix This will not be worked on #ffffff

New Issue Labels

Label Name Description Color
build system Issues related to building and packaging #f95f13
deployment project release & publishing #52794f
performance Issues related to application performance #cb738a
security bug reports, fixes, vulnerability issues #7057ff
testing project testing & validation #5319e7

Cloning labels from another project

If there is a repository that has a set of labels that can be used as a template for a new project, one can clone all of its existing labels to the new project with the command:

gh label clone -f kevinbowen777/django-start

Issue Label Examples

If you want to be selective, the labels can be individually added as follows:

gh label create "build system" -c F95F13 -d "Issues related to building, packaging"
gh label create deployment -c 52794F -d "project release & publishing"
gh label create performance -c CB738A -d "Issues related to application performance"
gh label create security -c 7057ff -d "bug reports, fixes, vulnerability issues"
gh label create testing -c 5319e7 -d "project testing & validation"

Other examples of gh label usage

As you can see, my usage of issue labels is pretty simple. Here are a couple of examples of label usage in more complex projects:

gh aliases

Some frequently used gh commands that I have converted to bash aliases:

alias ghicb='gh issue create --label bug --assignee @me'
alias ghicd='gh issue create --label documentation --assignee @me'
alias ghice='gh issue create --label enhancement --assignee @me'
alias ghict='gh issue create --label testing --assignee @me'
alias ghil='gh issue list --limit 100'

Resources

Tuesday, April 01, 2025

Customizing the Bash command prompt for vi editing mode

Customizing readline with inputrc

A brief introduction to the GNU Readline library, customizing user input using the .inputrc file, and the vi editing mode.

(Skip ahead to setting the mode indicator configuration.)

Introduction

I've been using vi/vim as my preferred editor long enough for the basic movement and editing keyboard shortcuts(also know as a key binding) to become second nature to me. Learning that the Bash shell supports multiple editing modes, including vi, gave me the opportunity to use this niche skill with other tools. I appreciate the idea that the key bindings that I had learned through using vi had some usefulness outside of a single application.

Bash supports this functionality through a library called Readline. Switching from Bash's default Emacs editing mode over to vi editing mode made sense, to me. Being able to switch tools and not have to remember a different set of keyboard shortcuts is convenient.

One feature that was missing, however, was a visual indicator of which mode I was currently in. For this, I needed to look at setting a couple of readline-functions that would modify the command line prompt to visually indicate whether one was in either insert or edit mode.

Setting the editing mode

By default, the Bash shell is set to use the readline Emacs editing mode.

To switch your command-line editing mode to vi, enter the following at the console:

set -o vi

If you are unsure which editing mode is currently being used in the shell, enter the command set -o from the command line. This will list the current state of the option-names and which editing mode is currently enabled:

The set command is a bash built-in that "sets or unsets values of shell options and positional parameters". See set --help for more details.

To make this change permanent, settings can be added in one of two files, depending on one's requirements.
For changing only the Bash shell to use the vi-editing mode, add the line set -o vi to the .bashrc.
Enabling the vi-editing mode for the Bash shell and any other tool that uses the Readline library, add the following line to the .inputrc file:

set editing-mode vi

From the readline(3) manpage:

Readline is customized by putting commands in an initialization file (the inputrc file). The name of this file is taken from the value of the INPUTRC environment variable. If that variable is unset, the default is ~/.inputrc. If that file does not exist or cannot be read, the ultimate default is /etc/inputrc.

Adding a Mode Indicator in the Prompt

Do you use set -o vi in your shell and can't remember whether you are in insert or edit/cmd mode? I do.

From the command line, activating a visual indicator at the prompt can be toggled with the following two commands:


bind 'set show-mode-in-prompt on'  
bind 'set show-mode-in-prompt off'  

From the man page:

If set to On, add a string to the beginning of the prompt indicating the editing mode: emacs, vi command, or vi insertion. The mode strings are user-settable (e.g., emacs-mode-string)

By default, this will place a string ((cmd)/(ins)) at the beginning of the prompt.

Moving between the modes can be done by pressing the Esc to enter the command mode and pressing the I key to resturn to insert mode.
This prompt can be customized with the vi-ins-mode-string and vi-cmd-mode-string variables.

If set from the command line, these indicators will persist only for the duration of that shell's existence.
To make the change permanent, add the following line to your .inputrc to place a visual indicator at the beginning of your prompt: set show-mode-in-prompt on

Personally, I prefer my command prompt slightly less cluttered. This is what my .inputrc file looks like for the mode indicators:


# Add mode indicators to command line
set show-mode-in-prompt on  
set vi-ins-mode-string "+"  
set vi-cmd-mode-string ":"  

This is my prompt with the customized mode indicators:

Different cursor shapes can also be configured for each mode. See the Arch Linux Readline wiki page for examples and a few more tips on modifying readline using the inputrc file.

vi editing mode

When opening a command line, the user will be in insert mode where text can be entered normally. When Esc or Ctrl+[ is pressed, an emulation of vi's cmd or normal mode will be entered and vi's keybindings can be used to move around and edit the command line. To return to insert mode, press the I key and text can be entered normally. Below is a list of some of the common key combinations:

  • 0 — Move to start of line
  • $ — Move to end of line
  • b — Move back a word
  • w — Move forward a word
  • e — Move to the end of the next word
  • dw — Delete a word
  • d$ — Delete to end of line
  • dd — Delete entire line
For a more complete list of commands, take a look at the Bash vi editing mode cheat sheet.

Thursday, March 20, 2025

Integrating fzf with the bash shell

command-line fuzzy finder(fzf) Shell Integration

Introduction

fzf is a highly customizable command-line fuzzy finder. Per the project's README:

It's an interactive filter program for any kind of list; files, command history, processes, hostnames, bookmarks, git commits, etc. It implements a "fuzzy" matching algorithm, so you can quickly type in patterns with omitted characters and still get the results you want.

Reviewing my notes, I realize that I'm barely scratching the surface of how fzf can be used and customized. For now, I'm going to outline how I've set up fzf for daily usage in the bash shell.

  • Installation and upgrading fzf
  • What key combinations can be used to fuzzy search through files and directories.
  • Customizing the finder display

Installation

This article assumes an fzf installation directly from GitHub and using the bash shell. For other installation methods and shell-dependent commands, refer to the fzf installation documentation.

  1. git clone --depth 1 https://github.com/junegunn/fzf.git ~/.config/fzf ~/.config/fzf/install

    Note that this is slightly different than the recommended install command. When I am able, I try to move installations off of the root of my home directory and, preferably, place them in the ~/.config/ directory.

  2. The installation script will prompt for three configuration options:

    • Do you want to enable fuzzy auto-completion? ([y]/n)
    • Do you want to enable key bindings? ([y]/n)
    • Do you want to update your shell configuration files? ([y]/n)

    Unless there is a compelling reason to do otherwise, accept the default choices.

  3. Here, the installation places the file .fzf.bash, which sets up the key bindings and completions, in the root of the home directory and modifies the .bashrc to source this file on startup. Again, I make some modifications to place this file in the ~/.config/fzf/ directory:
    • mv ~/.fzf.bash ~/.config/fzf/fzf.bash
    • Changes to fzf.bash:
      # Setup fzf
      if [[ ! "$PATH" == */home/kbowen/.config/fzf/bin* ]]; then
      PATH="${PATH:+${PATH}:}/home/kbowen/.config/fzf/bin"
      fi
      # Setup key bindings and shell completion
      eval "$(fzf --bash)"
    • Add the following line to .bashrc:
      # Set up fzf key bindings and fuzzy completion
      [ -f ~/.config/fzf/fzf.bash ] && source "$HOME/.config/fzf/fzf.bash"
  4. For reference, the keybindings and completion in fzf are provided by two files. In the installation directly from git, these files are located here:

      ~/.config/fzf/shell/key-bindings.bash
      ~/.config/fzf/shell/completion.bash
    

    Modifying the default installation is not required, just a personal preference of mine.

Upgrading fzf

fzf is actively developed and updated relatively frequently. Check for updates regularly with the command:

  • cd ~/.config/fzf && git pull && ./install
    For convenience, I've created an alias for this in my .bash_aliases file:
  • alias fzf_update='cd ~/.fzf && git pull && ./install && cd -'

Keybindings with fzf

Key Combination Description
CTRL-t Search all files and subdirectories of the working directory, and output the selection to STDOUT.
ALT-c Search all subdirectories of the working directory, and run the command cd with the output as argument.
CTRL-r Search your shell history, and output the selection to STDOUT.

CTRL-r is my most frequently used fzf command. Generally, when I want to run the previous command or the one before that, I'll use !! or !-2. After that, I don't keep the commands in my memory and using CTRL-r has become a great time-saver, for me.

Using CTRL-t is useful if you want to locate and run a command on a file. For example, to edit a file, type vim at the command line, then press the key combination <Ctrl-t> to perform a search. Finally, press <Enter> twice to open the file inside vim.

ALT-c is used for searching for and moving into a subdirectory.

Using the finder

Using any of these three key combinations will bring up a finder that will display a list(either of files, commands, or directories) and filter the results depending on the text entered.

Movement within the finder display

  • CTRL-K / CTRL-J (or CTRL-P / CTRL-N) to move cursor up and down
  • Enter key to select the item, CTRL-C / CTRL-G / ESC to exit
  • On multi-select mode (-m), TAB and Shift-TAB to mark multiple items
  • Emacs style key bindings
  • Mouse: scroll, click, double-click; shift-click and shift-scroll on multi-select mode

Customizing the finder display

By default, the finder will display a window without a file preview with the filter command line at the bottom of the display. With a little bit of tinkering, I've build a display with a preview window to show file contents as well as look aesthetically pleasing to me. For this, I needed to modify the FZF_DEFAULT_OPTS environment variable and add it to the .bashrc file:

# Set the fzf display layout
export FZF_DEFAULT_OPTS=" \
--multi \
--height=50% \
--margin=5%,2%,2%,5% \
--reverse \
--preview='(highlight -O ansi {} || bat {}) 2> /dev/null | head -n 50' \
--preview-window=45%,border-double --border=double \
--prompt='▶' \
--pointer='→' \
--marker='✗' \
--header='CTRL-c or ESC to quit' \
--color='dark,fg:magenta,border:cyan'"

See the fzf README for details on customizing the look of the display filter.

Thursday, February 27, 2025

Adding color to man pages

How to colorize man pages in your terminal

Adding color highlighting to the man pages is similar to code syntax highlighting. I find adding color to the pages helps differentiate variables, strings, and reserved words and makes it easier to visually parse the information presented. I accomplish this by passing ANSI color code parameters to the less pager used in display the man pages.

Configuration Steps

  1. Open your .bashrc file in an editor and add the following lines: 
  2.  
    LESS_TERMCAP_mb=$(printf "\e[01;31m") # enter blinking mode(bold red)  
    export LESS_TERMCAP_mb  
    LESS_TERMCAP_md=$(printf '\e[01;38;5;75m') # enter double-bright mode  
    export LESS_TERMCAP_md  
    LESS_TERMCAP_me=$(printf "\e[0m") # turn off all appearance modes  
    export LESS_TERMCAP_me  
    LESS_TERMCAP_se=$(printf "\e[0m") # leave standout mode  
    export LESS_TERMCAP_se  
    LESS_TERMCAP_so=$(printf "\e[01;33m") # enter standout mode(bold yellow)  
    export LESS_TERMCAP_so  
    LESS_TERMCAP_ue=$(printf "\e[0m") # leave underline mode  
    export LESS_TERMCAP_ue  
    LESS_TERMCAP_us=$(printf '\e[04;38;5;200m') # enter underline mode (magenta)  
    export LESS_TERMCAP_us  
      
    # Turn off Select Graphic Rendition(SGR)  
    # export GROFF_NO_SGR=1  
    # Start with color output disabled (avoid unexpected color code output)  
    export MANROFFOPT="-c"  
      
    # Set options for how less will be used as a pager. 
    # The MANPAGER environment variable will override 
    # settings used by the more general PAGER settings.  
    # In this case, I want to show percentage of page 
    # completion for man pages  
    export MANPAGER='/usr/bin/less -s -M +Gg'  
    export PAGER='/usr/bin/less -s -M'
    
      

  3. Source the .bashrc file (source .bashrc) to read and execute the newly added commands into the current shell.

Notes on groff rendering

Two lines of note in the above configuration: export GROFF_NO_SGR=1 and export MANROFFOPT="-c". One of these is needed in the configuration in order for the man page colorization to work. As I learned this week, there is an issue with newer versions of groff(i.e. newer than 2023)

See the following for details:
Groff + most change in behaviour
Displaying a man page on a terminal with/without my favorite pager shows garbage

Alternate Color Theme

Here is a slightly different color theme:

 
export LESS_TERMCAP_mb=$(printf '\e[01;31m') # (red)
export LESS_TERMCAP_md=$(printf '\e[01;35m') # (bold, magenta)  
export LESS_TERMCAP_me=$(printf '\e[0m')  
export LESS_TERMCAP_se=$(printf '\e[0m') 
export LESS_TERMCAP_so=$(printf '\e[01;33m') # (yellow)  
export LESS_TERMCAP_ue=$(printf '\e[0m') # 
export LESS_TERMCAP_us=$(printf '\e[04;36m') # (cyan)  

Generally, I have come to prefer separating out the color variable definitions from the statements exporting the variable as shown in the first configuration example. This alternate theme example will also work; but, shellcheck gives me an error: SC2155: Declare and assign separately to avoid masking return values. I try to pay attention to tools that I use when they make recommendations that could improve my code and configurations.

See Shellcheck error SC2155 for additional details.

How it all works

To quote liberally from Russell Parker’s post Adding Colors to man

To understand how the above environment variables work it helps to review what steps normally happen when viewing a manpage:

  1. man renders a page from a (likely compressed) nroff or troff/groff document and pipes the result it into the pager program, usually less
  2. If the piped text indicates formatting that needs to be performed then less has to figure out how to accomplish this for terminal output
  3. less uses the (deprecated) termcap database to look up how to achieve effects like underline and bold. In reality it ends up using termcap’s successor, the terminfo database, which maintains support for the termcap interface. This gives back an escape string which corresponds to the specified effect for your particular terminal
  4. Using these nifty escape strings, less finally displays the manpage to the user

Manpages use formatting like bold (for sections) and underlines (for things like file names and arguments). These should already work out of the box when using the man command but will not change the text color.

ANSI Color Codes

To see the ANSI color codes and how they would look on your specific terminal, here is a bash script to print them out.

 
#!/bin/bash  
# name: ansi_codes.sh (see References for link to source)  
for x in {0..5};  
  do echo === && for z in 0 10 60 70;  
  do for y in {30..37};  
  do y=$((y + z)) && printf '\e[%d;%dm%-12s\e[0m' "$x" \
  "$y" "$(printf ' \\e[%d;%dm] ' "$x" "$y")" && printf ' '; 
  done && printf '\n';  
done;  
done

The References section below contains links to several color charts.

References

Thursday, February 20, 2025

Rust and Cargo Cheat sheet

Installing Rust, its crates and how to keep them up to date

Jump to the rust and crate management section for a list of commonly used commands.

Introduction

A couple of years ago, I found a utility called bat that I wanted to try out and possibly use as a partial replacement to the venerable print and concatenation tool named cat. It was written in a program language called Rust which I had heard about in passing and was curious about its rising popularity. The version that was currently available for my Debian system, at the time, lagged a few versions behind the one that I had read about and lacked some of the features that had initially piqued my interest.

I was already using pyenv to manage my Python releases and, I wondered if I could do the same for Rust. I haven’t really had the time, or inclination to become a Rust programmer; but, there were some applications written in Rust that I wanted to use. I wasn’t planning on installing many applications or updating them very frequently, so I needed a place to keep commands that my muscle memory hadn’t yet absorbed.

That’s how this cheat sheet initally started.

This is a very brief guide for installing Rust and managing those crates(applications, packages, or libraries) for people who are not Rust developers and who do not use Rust on a daily basis. It is not intended as a tutorial(see the Resources section for helpful links).

Getting Started

  • The Rust Getting Started page is a good place to begin learning about Rust, its eco-system, and how to create new Rust projects. Here, I’m just going to focus on getting Rust installed and the basics of using cargo, the Rust package manager, to install and upgrade crates. I’ve only used the commands on this page with Debian Linux. So, if you have any issues using them, I would strongly recommend looking at the official Getting Started page for additional information.

Common Rust applications

  • rustc - Rust interpreter
  • rustup - Rust tool chain installer & version management tool
  • cargo - Rust build tool & package manager
  • crates.io - A Rust community registry of libraries and applications

Rust Installation

From your command prompt, run the following:

~$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

By installing this way, rustup will install itself, the Rust tool chain, as well as the cargo package manager. Rust can be uninstalled at any time using the rustup self uninstall command and these changes will be reverted.

Default directories

Rustup metadata and toolchains are installed into the Rustup home directory, located at ~/.rustup. This location can be modified with the RUSTUP_HOME environment variable.

The Cargo home directory is located at ~/.cargo. This location can be modified using the CARGO_HOME environment variable.

The cargo, rustc, rustup and other binaries are added to Cargo’s bin directory, located at ~/.cargo/bin.

Rust and crate management

Steps for upgrading Rust

  • Check the currently installed Rust version
    rustc -V
  • Update Rust
    rustup update
  • Update rustup
    rustup self update

Updating crates

As of Rust 1.41.0, cargo install <crate> will detect if a package is installed. It will upgrade if there is a newer version, or do nothing if the crate is considered up to date. The command will always uninstall, download, and compile the latest version of the crate - even if there is no newer version available.

The cargo-update crate

This crate creates a cargo subcommand for checking and applying updates to installed executables. It helps simplify crate management.

  • cargo-update home page
  • Install cargo-update
    cargo install cargo-update
  • Check for newer versions and update selected packages
    cargo install-update -a

Steps for installing and upgrading crates

  • List installed crates
    cargo install --list
    • Or, add an alias to .bashrc or bash_aliases:
      alias rust_list='cargo install --list
  • Install a crate
    cargo install <package_name>
  • Update all crates
    cargo install-update -a
  • Check for newer versions and update selected packages
    cargo install-update crate1 crate2 ...

Currently Installed Crates

Crates that I have currently on my system and find useful

  • bat A cat(1) clone with syntax highlighting and Git integration.
  • cargo Cargo downloads your Rust project’s dependencies and compiles your project.
  • cargo-update A cargo subcommand for checking and applying updates to installed executables
  • csview A high performance csv viewer with cjk/emoji support
  • htmlq A lightweight, command-line HTML processor
  • iamb A Matrix chat client that uses Vim keybindings
  • zizmor A Static analysis tool for GitHub Actions

Resources

Tuesday, February 11, 2025

Our cat's medical bills

I wasn't expecting the subject of my first post here to be about my cat Anu;
but, if this is the impetus to get me to writing more publicly then I'll use
that as my motivation.

Early last week our cat, Anu, had to be taken in to the emergency care clinic
for surgery to save her life. We had noticed a slight loss of appetite and a
bit of lethargy. My partner got home from work around 7 o'clock on Monday and
noticed some pus/discharge. I had seen her a couple hours before and she had
seemed fine. We remained at the clinic until around four in the morning after
Anu had come out of surgery and we were given a positive prognosis. Much of the
detail of the night is pretty emotional and jumbled, for me, since everything
happened so quickly.

One of the decisions we had to make was having to sign and agree to payment
before most Anu's medical care could proceed. We wouldn't have made a different
decision; but, having to do that in the midst of an already highly emotional
event was frustrating to say the least. We are now looking at a medical bill
of around nine thousand dollars that we really cannot afford. This is the
reason for my posting today. We could really use some financial assistance to
help us get by.

It's been a week since Anu came home. She seems to be recovering well.
We've completed her course of antibiotics and kitty sedatives. She's not eating
as much as I like; but, I think much of that may be due to her cone-of-shame
somewhat hindering her mobility. We've got a follow up appointment with her
regular veterinarian tomorrow.

So I am asking anyone reading to consider helping us out financially to pay down
this bill. Any amount will help and will be deeply appreciated.

I was intending for my posts here to focus more on my technical, open source
contributions and work. Hopefully, with your help, I can start to do that in
the future.

Thanks for reading this. I appreciate it.

My ko-fi page
My LiberaPay page

Wednesday, December 15, 2021

Creating git repositories on public source forges (GitLab, GitHub...)

Creating git repositories on public source forges

Last updated: 20211215
 

 Create local directory:

 
      mkdir <repository name> && cd $_
 

Create repository on GitLab/Hub:

 
      GitLab:
          Menu -> Create new project -> Create blank project
      GitHub:
          TBD
 

Initialize local repository and push initial commit:


      git init --initial-branch=master
      git config --global user.name "Kevin Bowen"
      git config --global user.email "kevin.bowen@gmail.com"
      git remote add origin git@gitlab.com:kevinbowen/sampleproject.git
      # git remote add origin git@github.com:kevinbowen777/sampleproject.git
      git add .
      git commit -m "initial commit"
      git push -u origin master

Thursday, December 20, 2018

Upgrading OpenSSL from 1.1.0 to 1.1.1 in Linux Mint 19 and Ubuntu 18.04



According to the OpenSSL website:

> The latest stable version is the 1.1.1 series. This is also our Long Term Support (LTS) version, supported until 11th September 2023.

Since this is not in the current Ubuntu repositories, you will need to download, compile, and install the latest OpenSSL version manually.

Below are the instructions to follow:

1. Open a terminal (Ctrl+Alt+t).
2. wget https://www.openssl.org/source/openssl-1.1.1a.tar.gz
3. Unpack tarball with `tar -zxf openssl-1.1.1a.tar.gz` and then `cd openssl-1.1.1a`.   
4. cd to `openssl-1.1.1a`
3. Issue the command './config'.
4. Issue the command 'make' (You may need to run `sudo apt install make gcc` before running this command successfully).
5. Run `make test` to check for possible errors.
5. Issue the command 'sudo make install'.
6. Backup current openssl binary:
    sudo mv /usr/bin/openssl ~/tmp
7. Create symbolic link from newly install binary to default location:
    sudo ln -s /usr/local/bin/openssl /usr/bin/openssl
8. Run command 'sudo ldconfig' to update symlinks and rebuild library cache.

Assuming that there were no errors in executing steps 3 through 6, you should have successfully install the new version of OpenSSL.

Again, from the terminal issue the command
    openssl version
Your output should be as follows:
    OpenSSL 1.1.1a  20 Nov 2018

Thursday, December 13, 2018

Pre-flight steps for a Linux Mint desktop upgrade


Upgrading from Linux Mint from 18.3 to 19 on my systems was essentially painless.

There are a couple of things that I have gotten into the habit of doing prior to performing systems upgrades to provide me some peace of mind:

  1.  Back up your system.
  2.  Separately back up your .config and .local directories (I am selective in choosing which application preferences to preserve across machines and store them on dropbox).
  3.  Make a list of any PPAs you might be using and remove them from your sources.list or Update Manager. Here is a pointer to a script to get you started.
  4.  I store my dotfiles on a repo in GitHub (and replicate them out to GitLab and BitBucket), so that I can easily restore my bash, vim and tmux settings.
  5.  Make a list of your favorite apps (Personally, I use vimwiki stored in dropbox for my sysadmin notes).
  6.  I've found the following site very useful when turning up a new system: https://sites.google.com/site/easylinuxtipsproject/Home (It's Mint-centric and has some very sane recommendations. Don't run the suggestions blindly. Review them closely and see if it applies to you.
  7.  Maintain a separate /home partition so that you can easily restart, or completely blow away, an installation if you need to without fear of losing your personal data.

This may sound like a lot of work to those just getting started in the Linux/Mint world; but, some conscientious janitorial work upfront saves one from frustration down the road.

Hope this helps. Enjoy the upgrade!

Tuesday, July 19, 2016

Switching between Java versions in Debian based distributions

Switching between installed Java versions can be accomplished using the update alternatives command.

To get a list of your installed Java platforms, run the following command from the terminal:

sudo update-alternatives --config java

This will give you a list output similar to this:

There are 2 choices for the alternative java (providing /usr/bin/java).
   Selection                             Path                                   Priority         Status
  ---------------------------------------------------------------------------------------------
  0            /usr/lib/jvm/java-8-oracle/jre/bin/java                1081            auto mode
*1            /usr/lib/jvm/java-7-openjdk-i386/jre/bin/java    1071       manual mode
  2           /usr/lib/jvm/java-8-oracle/jre/bin/java                 1081       manual mode
Press enter to keep the current choice[*], or type selection number:

In this case, the Open JDK 6 version is running. To switch to the Open JDK version 7, you would select option 1 at the prompt and press the Enter key.

You will now be running the OpenJDK 7 version. No other changes will be needed to switch your Java versions.

Wednesday, April 08, 2009

Bands I have seen in concert

Yet again, I continue with publishing useless lists
Here is one, created from memory, of all of the bands that I have see perform live.

I'm sure there are many bands that I have forgotten.
I'm actually pretty amazed that I have remembered as many as I did.

I would love to see the lists of other folks. Make fun of me as you like.


Bands I have seen in concert:

Agnostic Front
Agent Orange
Alice Cooper
Anthrophobia
Arab Strap
Baby Flamehead
Big Smelly Fish
Black Flag
Bloodloss
Bongwater
The Boredoms
Boss Hog
Butthole Surfers
Buzzcocks
John Cage
Caspar Brotzmann Massaker
Nick Cave
Nick Cave and the Bad Seeds
Cloaca
Cocteau Twins
Cop Shoot Cop
Cornershop
The Creatures
Cro-Mags
The Cure
Dead Moon
Miles Davis
Dirty Three
Disgruntled Postal Workers
Mike Doughty
Dancing French Liberals of '48
Dead Can Dance
Deadspot
The Decemberists
Dee-lite
Dio
The Fastbacks
GBH
George Clinton and the P-Funk Allstars
Gene Loves Jezebel
Glass Eye
God Bullies
Grateful Dead
Groovie Ghoulies
Grotus
GWAR
Homo Picnic
Imperial Teen
Jesus Lizard
Jet
Jon Spencer Blues Explosion
Judas Priest
King Carcass
The Knitters
Mark Kozelik
Kultur Shock
Laughing Hyenas
Love Battery
L7
Lubricated Goat
Lungfish
The Melvins
Mission of Burma
Moby
More Fiends
Mudhoney
Murphy's Law
Nashville Pussy
Nebula
New Order
Onyx
Pagan Babies
Painkiller
Pain Teens
Pigface
Piss Drunks
The Pleasure Elite
Psychic TV
Red Hot Chili Peppers/Fishbone
R.E.M.
The Replacements
Reverend Horton Heat
RUIN
Sage
Seals & Crofts
Sebadoh
She-Males
Sink Manhatten
Siouxsie and the Banshees
Sky Cries Mary
Sleater-Kinney
Slint
The Smiths
Sonic Youth
Spore
Stereolab
Ken Stringfellow
Suicidal Tendencies
Sun Kil Moon
Sunny Day Real Estate
TAD
10,000 Maniacs
Token Entry
Tom Tom Club
Tons of Nuns
Visqueen
Yo La Tengo
X

Thursday, April 05, 2007

Bitch Kitty Racing - My latest project

Bitch Kitty Racing is an online "lifestyle and entertainment" magazine. For the last month or two, I have been assisting in the ongoing maintenance and promotion of the site.

John Moroney invited me to join himself and his co-founder, Keith Bingman in their quest for world domination. Besides being involved in a cool project and building a loyal readership, I've been busy learning some technologies that I haven't gotten my hands dirty with previously, such as RSS(news) feeds, podcast creation, and distribution, and testing.

The site, itself, is powered by a light-weight, highly flexible Ruby on Rails content management system called Radiant. Now, while I don't consider myself a developer, by any stretch of the imagination; I see this as a good opportunity to collaborate on a cool project, and try to get up to date on some cutting edge web applications.
I'm hoping , as time goes on, to also pick up a bit of the ruby programming basics.

Keith is the driving force behind the site design and functionality. He is also actively involved in the developer community behind the Radiant application. At this moment, he is creating extensions to help us manage the images and comments coming soon to Bitch Kitty Racing. The first is especially relevant since Keith is also a pretty good photographer. You should take a look at his work, here: keithbingman.com

John is the ringleader of the site and primary author of most of the content. He is also the star of the Bitch Kitty Racing TV podcasts. It's pretty funny, bizarre, and probably not safe for children, small woodland creatures, or work. We even made it onto ITunes with the series. Fun stuff.

Let's see...What else can I pimp while I am at it? I hadn't really intended to make this a Bitch Kitty Racing promotional weblog post; but....

Ah yes, we are also trying our hand at some band promotion with a local Seattle band, Hey Marseilles. Rumor has it that the big labels are looking at them, so we are enjoying promoting them while we can.
All in all, Bitch Kitty Racing has been keeping me very busy learning new skills.

Thursday, March 08, 2007

Death of (another) philosopher

I'm sure that relatively few people will care about this passing.
I was just listening to podcast from the NPR program,Fresh Air, and discovered that the French post-modern philospher, Jean Baudrillard
(http://en.wikipedia.org/wiki/Jean_Baudrillard), had just recently died.

I can't say that I am sad, or disheartened. Rather, I am somewhat amused by his passing. He is yet another of my self-appointed intellectual mentors who has passed on.
Gone beyond, so to speak, the illusion, that is the life he had lived.

Call me what you will; but, I found his philosophy to be supremely influential upon my general worldview(along with Foucault, and Deleuze/Gauttari), and appreciate his contribution to the world, and human thought, in general.
I had always appreciated his willingness to tell the emperor(you, the reader) that not only did he(you) really have no clothes, but he wasn't even actually an emperor. Now, don't get me wrong, I don't consider Mssr. Baudrillard a nihilist. Far from it.
I actually consider him a comic, in a sense, perhaps in the most generous sense. He was a philosopher in the truest sense. But, in this day and age, philosphers are not needed by most people. So, instead, he, and his wrtings, were relegated to the backwaters of the media and inteligensia as the ramblings of an esoteric crank. Or, at worst, his messages were inscrutable, and thus irrelevant. I suppose that he might have appreciated the irony in that. I have a way with picking the winners...

For better or worse, I guess his most famous quote is from the film "The Matrix". It is the character Morpheus, played by Laurence Fishburn( whom I loved in Apocalyspse Now) that states to that dumbass initiate, Neo, "Welcome to the desert of the real."
He died on my mother's fifty-ninth birthday.

You will be missed, funnyman.

Wednesday, January 17, 2007

T.S. Eliot - The Waste Land

I've found some audio files of T.S. Eliot reading his classic poem "The Waste Land."



Born in St. Louis, Missouri, and educated at Harvard, Eliot lived most of his life in England. In 1948 he was awarded the Nobel Prize. The poem has five sections and has been split into four sound files:

The Waste Land is considered to be Eliot's masterpiece, rich in symbolic, literary, and historical references as the poem explores the struggles of a soul in despair.


Funny, this isn't how it sounds when I read it to myself...


For additional information and references, see Wikipedia::The Waste Land
N.B. These audio files were originally hosted at media.org. The site contains a large collection of media from the early days of the Internet rescued from digital oblivion. The HarperAudio section contains several dozen audio files of poems and excerpts from novels being read by their authors. Faulkner, Burgess, Hemingway, Thomas are just a few.
Even Shakespeare reading some of his sonnets!

Friday, January 12, 2007

RIP: Robert Anton Wilson (1932-2007)

Today, I was saddened to learn that one of my favorite guerrilla ontologists, Robert Anton Wilson, had passed away, this week, on January 11th.

My first exposure to Mr. Wilson's ideas came through reading the Illuminatus! Trilogy which he co-authored with Robert Shea. I managed to stumble upon his work while in college, and found it to be a great balance against all of the "serious" and "deep" philosophical and ontological works I was digesting at the time. I found it to be an astounding compendium of ideas, both revelatory and fanciful, simultaneously conspiratorial, paranoid, and yet somehow playful and optimistic. His works were a welcome relief from the heaviness of German phenomenologists, and occult cranks.

However, it was his other books that helped change the way I think, philosophically, about a lot of things, particularly his 'non-fiction' writings like Cosmic Trigger and Prometheus Rising. Specifically, I credit Mr. Wilson with helping me to allow humor into my particular Weltangshauung, as well as providing some cognitive tools that allowed me to understand socially constructed belief systems, or reality tunnels, as he called them.

Here is one little factoid about Mr. Wilson, that I only learned about upon his recent passing:

"As a member of the Board of Advisors of the Fully Informed Jury Association, he worked to inform the public about jury nullification, the right of jurors to nullify a law they deem unjust."

R. A. Wilson's Home Page

Obituaries:
Blog Critic
R.U.Sirius
Paul Krassner
BoingBoing
Blather.net
Mercury News