Replacing new lines with nulls in bash

If you have a list of file names separated by new lines, and you want to turn it into a list of file names separated by null characters, for instance for use as input to xargs, then you can use the tr command, like this:

  $ cat /path/to/new-lines | tr '\n' '\0' > /path/to/null-separated

I found a whole article on various ways to replace text on *nix: rmnl — remove new line characters with tr, awk, perl, sed or c/c++.

Extracting a single file from a tar archive

You can use a parameter to the -x command line switch to tell the tar command that you just want to extract one particular file from the archive. For example:

  $ tar -x filename/to/extract -f tarfile.tar

You can use the -O command line switch in conjunction with the above to have the file’s contents printed on stdout rather than created in the file system.

Changing parent’s directory from a subshell

I was trying to figure out how to have a child process change the current directory of a parent process. Turns out you can’t quite do that. One thing I guess you could do is export a global variable and then have the parent check that when the child returns, but I decided on another approach. What I do is write a file to ~/bin/errdir that contains a single line in the format:

  pushd "/path/to/directory"

I can then source this as a command file after my script has run to change directory into the directory where the last erroneous file was encountered with the command:

  $ . errdir

When I’m finished I can just type ‘popd’ to return myself to wherever I was before I changed into the error directory.

Error handling in bash

I’ve been learning about approaches to error handling in bash. I found Error handling in BASH, Exit Shell Script Based on Process Exit Code, Writing Robust Bash Shell Scripts and Bash: Error handling all of which contained a nugget of useful information.

Particularly I learned about $LINENO, set -e (equiv: set -o errexit), set -o pipefail, set -E, set -u (equiv: set -o nounset), set -C (equiv: set -o noclobber), #!/bin/bash -eEu, trap, shopt -s expand_aliases, alias, $PIPESTATUS, subshells, unset, and more.

If you call exit without specifying a return code then $? is used, I didn’t know that.

I found this code snippet that demos a relatively race-condition free method of acquiring a lock file:

if ( set -o noclobber; echo "$$" > "$lockfile") 2> /dev/null; 
then
   trap 'rm -f "$lockfile"; exit $?' INT TERM EXIT

   critical-section
   
   rm -f "$lockfile"
   trap - INT TERM EXIT
else
   echo "Failed to acquire lockfile: $lockfile." 
   echo "Held by $(cat $lockfile)"
fi 

Unix command to format a number of bytes as a human readable value

It took me a while, but I finally figured out how to print a number from a bash script properly formatted with commas as thousand’s separators. For those like me who weren’t in the know, the magical incantation is:

  printf "%'d" 123456

That will format 123456 as 123,456. Much easier to read when you’re dealing with large numbers such as the sizes of files in gigabytes.

So now if I could only find the Unix command that took a number of bytes and turned it into an approximate value with GB or MB suffixes.

Auto-extracting archives

I have a directory structure with archived files, many of which are zipped or tarballed up. So if I want to search for a file, I can’t really be sure that the file I’m looking for isn’t in a compressed file. So I wrote some scripts to automatically run over a directory tree and automatically extract any compressed files that it finds there. There are a few extra features, such as if the file is larger than 10MB then it will prompt for whether to extract it or not. I have a few other features for handling errors to add in, but I’m happy to post this version up now.

extract-archives.sh

#!/bin/bash
err=$1
err=${err:-1}
search() {
	find -iname "*$1" -print0 | xargs -i -0 `dirname "$0"`/extract-file.sh "$1" "$2" "$err" "{}"
}
search ".tar.gz" "tar xf" 
search ".tgz" "tar xf" 
search ".zip" "unzip -q" 

extract-file.sh

#!/bin/bash
exec 0< /dev/tty
path="$4"
file_name=`basename "$path"`
dir_name=`dirname "$path"`
new_name=`basename "$path" "$1"`
new_path="$dir_name/$new_name"
[ -e "$new_path" ] && exit 0
echo $dir_name/$file_name
file_size=`stat -c %s "$path"`
#echo -n "File is $file_size bytes."
printf "File is %'d bytes." $file_size
check=`echo "$file_size < 10485760" | bc`
if [ "$check" = "1" ]; then answer="y"; fi
while [ "$answer" != "y" ]; do
	read -n 1 -s -p " Extract? " answer
	[ "$answer" = "n" ] && echo && echo && exit 0
done
echo
mkdir "$new_path"
pushd "$new_path" > /dev/null 2>&1
$2 "../$file_name"
if [ "$?" != "0" ]; then
	popd > /dev/null 2>&1
	rm -rf "$new_path"
	echo
	answer=""
	while [ "$answer" != "y" ]; do
  		read -n 1 -s -p "Do you want to ignore this file? " answer
		[ "$answer" = "n" ] && exit $3
	done
fi
popd > /dev/null 2>&1
echo