Command line tools

Last update: February 13, 2023

I am a big fan of command line utilities and scripts, so I wanted to share with you some tricks I learned and scripts I use. Some are only a command with notable options like a memo of how to use them, others are homemade scripts.

Find allows you to apply some quite powerful command to your files, you can query it to get only specifics files and then pipe them to other commands.

# Exec a given command (rm -f here)
find . -type f \  # Find all file (only of type: file, not directories)
    ! -mtime 60 \ # Where modification time is greater (!) than 60 days
    --exec rm "{}" -f \;

# Note: using xargs is faster, so we can do something similar:
find . -name .svn \ # Find all file with name '.svn'
    -print0 \       # Send the list to (null padded)
    | xargs -0 \    # Get the list from (null padded)
    rm -rf # Current command (rm -f here)

# Find file by extended regex
find -E . -regex ".*\.(php|sh)"

xargs allows you to build/execute a command from an input that you get on the pipe.

# -I % defines "%" as the placeholder
cat foo.txt | xargs -I % sh -c "echo %; mkdir %;"

Find in combination with other utilities

# Copy all mp4 file to /tmp
find . -type f -iname "*.mp4" -exec cp "{}" /tmp \;

# Update all C Sharp files "end of line" from windows to unix (dos2unix)
find . -type f -iname "*.cs" -exec dos2unix "{}" \;

# Rename all file, replacing 'Screenshot-' by 'Screen_'
find . -exec rename -s "Screenshot-" "Screen_" "{}" \;

# For each jpg, get the exif meta or add a macos tag if not found (-I % defines the placeholder)
find . -iname "*.jpg" -print0 | xargs -I % -0 bash -c 'exiftool "%" | grep -i "create date" || tag -a missingExif "%"'

# You can have complex queries for find with \( \) and -o
find . \( -iname "*.html" -o -iname "*.php" \) -print0 | xargs -I % -0 bash -c 'echo "%";'


Curl makes web requests, it's very powerful for basic and not-so basic requests, allowing you to download files, forge request and even interact with APIs.

curl \
    -d "param1=value1¶m2=value2" \ # Post data
    -H "Content-Type: application/x-www-form-urlencoded" \ # This is the default with POST
    -X POST \ # Method

curl \
    -d "@data.txt" \ # Send this file content as post data
    -X POST \

curl \
    -F [email protected] \ # Upload with multipart/form-data content
    -X POST \

curl \
    -d '{"key1":"value1", "key2":"value2"}' \ # Post data, here JSON
    -H "Content-Type: application/json" \     # Specify JSON header
    -X POST \


Rsync is powerful and versatile, I'm using it for backups and remote server sync, but it can do way more!

# Here I use it for basic one-way no-history archiving
rsync \
    --archive \ # implies: recursive ; preserve time, owner, group, perms ; copy symlinks as it
    --human-readable --progress \
    --delete \ # Delete the file on destination if not present on the source any more
    --exclude='node_modules' \ # Node ;-)
    /Users/jerome/projects /Volumes/Backup-disk # Source / Destination

# Synchronize the local directory with the distant one
sync --archive --verbose --delete \
    -e "ssh -p 22" \ # Remote shell to use (here ssh with option -P 22)
    static/ [email protected]:/var/www/

Shell commands 101

# Redirect the error output to the standard output (stderr to stdout)
echo 'test' 2>&1

# Mount a distant directory to the local filesystem
sshfs [email protected]:/directory directory

# Delete the line LINE_NUMBER in the file
sed 'LINE_NUMBERd' file
sed '2,4d' file # delete lines from 2 to 4 in file

# Stop/continue a process by pid

# Create an archive for this directory
tar -cvzf archive.tar.gz directory

# Extract the given tarball
tar -xvf archive.tar.gz

# Generate a new key pair
ssh-keygen -t dsa

# Change extended permissions on a file
setfacl -Rm u:username:rw directory

# Convert windows line ending file to unix

# Add user (username) to group
usermod -a -G group username


I use ngrok

This is a proprietary tool, but still, it has a free tier that is enough for me.
It may exist some similar tools, do not hesitate to contact me!

Ngrok exposes local servers behind NATs and firewalls to the public internet over secure tunnels.

Linux server

UFW Firewall

On my Linux server I use ufw (Uncomplicated Firewall), and it holds its promise! See it in action below:

ufw allow 22 # ssh
ufw allow 443 # https
ufw allow 80 # http
ufw enable


I use Mosh

Mosh (mobile shell)

It's a remote terminal application that allows roaming, supports intermittent connectivity, and provides intelligent local echo and line editing of user keystrokes.

Mosh is a replacement for interactive SSH terminals. It's more robust and responsive, especially over Wi-Fi, cellular, and long-distance links.

Mosh is free software, available for GNU/Linux, BSD, macOS, Solaris, Android, Chrome, and iOS.


Create a new named session: screen -S NAME Restore a named session: screen -x NAME


  • Ctrl + a c = create
  • Ctrl + a Shift + A = title
  • Ctrl + a Ctrl + a = toggle
  • Ctrl + a num = switch to num
  • Ctrl + a Shift + F = fit
  • Ctrl + a space/backspace = next/prev windows
  • Ctrl + a Shift + C = clear
  • Ctrl + a " = List tabs


termcapinfo xterm* [email protected]:[email protected] # Pour le scroll
caption always # activates window caption
caption string '%{= wk}[ %{k}%H %{k}][%= %{= wk}%?%-Lw%?%{r}(%{r}%n*%f%t%?(%u)%?%{r})%{k}%?%+Lw%?%?%= %{k}][%{b} %d/%m %{k}%c %{k}]' # good looking window bar
bindkey -k k5 prev # F5 for previous window
bindkey -k k6 next # F6 for next window

My Own scripts

filename / extname

Similar to basename, I've often needed to get the filename (without extension) or the extension of a given file in my scripts.
I added them in my path, so they are accessible when needed.



if [[ $# -ne 1 ]]
    echo "usage: $0 \"filename.ext\""
    echo "Returns filename without extension"
    echo "Note: it applies basename before"
    exit 2

BASENAME="$(basename "$1")"

echo "${FILENAME}"
exit 0



if [[ $# -ne 1 ]]
    echo "usage: $0 \"filename.ext\""
    echo "Returns filename's extension"
    echo "Note: it applies basename before"
    exit 2

BASENAME="$(basename "$1")"

echo "${EXTENSION}"
exit 0


This script is more specific to my needs, but still I think it can be useful to someone.
It will try to rename a media file with the date/time of creation.
Note: it uses PHP and exiftool to work.


#!/usr/bin/env php

function stdout($message)
    echo $message."\n";

if (2 != count($argv)) {
    stdout('Rename media files to their original creation time (if possible)');
    stdout("Usage {$argv[0]} source_media");


function exiftool(string $file): array
    $output = shell_exec('exiftool '.escapeshellarg($file));
    $data = [];
    foreach (explode("\n", $output) as $line) {
        $l = explode(':', $line, 2);
        $data[trim($l[0])] = trim($l[1]);

    return $data;

$file = $argv[1];
$directory = dirname($file);
$extension = pathinfo($file, PATHINFO_EXTENSION);
$data = exiftool($file);

if (isset($data['Creation Date'])) {
    $exifDate = $data['Creation Date'];
} elseif (isset($data['Date Time Original'])) {
    $exifDate = $data['Date Time Original'];
} else {
    stdout("Can not find relevant exif information for {$file}");


$date = date_create($exifDate);
if (!$date) {
    stdout("Can not parse date for {$file}");


$name = $date->format('Y-m-d H-i-s').'.'.$extension;
$destination = $directory.DIRECTORY_SEPARATOR.$name;

if (file_exists($destination)) {
    stdout("A file with that name already exist {$name} for file {$file}");


rename($file, $destination);
$touchDate = $date->format('YmdHi.s');
shell_exec('touch -t '.$touchDate.' '.escapeshellarg($destination));
stdout("Success: {$file} renamed in {$destination}");



This script looks for file doubloons in the current and sub-directories.
Note: it's a PHP script that use a SQLite database.


#!/usr/bin/env php

const DB_FILE_NAME = './doubloon.sqlite';
$shortOptions = [];
// Index options
$shortOptions['e:'] = 'File extension to check ; coma separated ; case insensitive ; default: "jpg,jpeg,gif,png"';
$shortOptions['s:'] = 'Hash algorithm used ; available: md5, sha1 ; default "md5"';
$shortOptions['r'] = 'Reset/rebuild index';
// Program options
$shortOptions['f'] = 'For a given doubloon ; keep the first and delete the others ; default: interactive mode: ask';
$shortOptions['h'] = 'This help';

$options = array_merge([
    'e' => 'jpg,jpeg,gif,png',
    's' => 'md5',
], getopt(implode(array_keys($shortOptions)), []));

function stdout($message)
    echo $message."\n";

function sqlite(bool $create)
    $sql = new SQLite3(DB_FILE_NAME);
    if ($create) {
        $sql->exec('CREATE TABLE "file" ("path" text NOT NULL, "hash" varchar NOT NULL, PRIMARY KEY (path));');

    return $sql;

function findDoubloons()
    $hashes = [];
    $db = sqlite(false);
    $result = $db->query('SELECT * FROM `file` WHERE `hash` IN (
        SELECT `hash` FROM `file` GROUP BY `hash` HAVING COUNT(*) >= 2
    while (($row = $result->fetchArray(SQLITE3_ASSOC))) {
        $hashes[$row['hash']][] = $row['path'];

    return $hashes;

function index(string $directory, string $extensions, string $hashAlgo)
    stdout('Building index...');
    $extensionsRegex = '`\.('.str_replace(',', '|', preg_quote($extensions)).')$`i';
    $find = shell_exec('find -L "'.$directory.'"'); // -L to follow symlinks
    $data = explode("\n", $find);
    $db = sqlite(true);
    foreach ($data as $line) {
        $line = trim($line);
        if (!preg_match($extensionsRegex, $line)) {
        if ('sha1' === $hashAlgo) {
            $hash = sha1_file($line);
        } else {
            $hash = md5_file($line);
        $statement = $db->prepare('INSERT OR IGNORE INTO `file` VALUES (:path, :hash);');
        $statement->bindParam(':path', $line, SQLITE3_TEXT);
        $statement->bindParam(':hash', $hash, SQLITE3_TEXT);

        stdout($line.': '.$hash);



function remove($interactive)
    $hashes = findDoubloons();
    $delete = [];
    $progress = 0;
    $total = count($hashes);
    stdout('Started, type 99 to finish...');
    foreach ($hashes as $hash => $paths) {
        stdout("Found doubloons ({$progress}/{$total}):");
        stdout('    0. No action');
        $candidates = [];
        $c = 0;
        foreach ($paths as $path) {
            $candidates[$c] = $path;
            stdout("    {$c}. {$path}");
        if (!$interactive) {
            $action = 1;
        } else {
            $action = (int) trim(readline("> keep [0-{$c}]: "));
        if (0 == $action) {
        if (99 == $action) {
        $delete = array_merge($delete, array_values($candidates));
    stdout("Double check everything and execute:\n");
    foreach ($delete as $d) {
        stdout('rm "'.$d.'"');
    stdout('rm '.DB_FILE_NAME);

    return $delete;

if (array_key_exists('h', $options)) {
    stdout("Usage: {$argv[0]} [options]");
    foreach ($shortOptions as $name => $message) {
        $name = trim($name, ':');
        stdout("    -{$name}    {$message}");


if (array_key_exists('r', $options)) {

if (!file_exists(DB_FILE_NAME)) {
    // No index exist in this directory, build one
    index('.', $options['e'], $options['s']);
} else {
    // the index already exist, start the program
    remove(!array_key_exists('f', $options));

Other custom-made scripts

I made other scripts that are probably too specific and too messy to be published here, one I like a used a lot was a PHP script (+ command line tools) that split a mbox file and extract message/attachment to a database. Worked well to save my emails when I left Gmail. Let me know if you are interested :)