Today I Learned

hashrocket A Hashrocket project

189 posts by dillonhafer twitter @               

Keep 5 most recent files in a directory

One can keep the most recent n files in a directory with just three shell programs: ls, tail, and xargs.

Here is an example to use in a nightly database backup cron job:

#!/bin/bash
# Keep last 5 files ending in .dump
# Don't forget to 

# Installation
# 1. cp pg-backups.sh /usr/local/bin/
# 2. chmod u+x /usr/local/bin/pg-backups.sh
# 3. Set the DB variable
# 4. Set the BACKUP_DIR variable

# Example usage for cron to run at 4:05 am every day:
# 5 4 * * * /usr/local/bin/pg-backups.sh

DB=mydatabase
BACKUP_DIR=/mnt/object/production/db-backups

DATE=$(date "+%Y-%m-%d_%H%M")
pg_dump -Fc $DB > $BACKUP_DIR/$DATE.dump

/bin/ls -t $BACKUP_DIR/*.dump | tail +6 | xargs rm

Use created_at in Ecto

You can use created_at in Ecto/phoenix app with timestamps/1. When migrating data from a rails application to a phoenix application you will have many tables with a created_at column.

defmodule Phoenix.Accounts.User do
  use Ecto.Schema
  import Ecto.Changeset

  schema "users" do
    field :email, :string
    field :password, :string, virtual: true, redact: true
    field :hashed_password, :string, redact: true
    field :confirmed_at, :utc_datetime

    timestamps(inserted_at: :created_at, type: :utc_datetime)
  end

end

How to install sqlite3 on heroku

Using sqlite to persist data is superfluous on heroku, duh, but sometimes a third party service wants my rails app to read configuration in a sqlite db file. In order to read the read-only database file, I need to install the sqlite3 gem. To get this to work on heroku I needed to do two things:

  1. Install the apt buildpack
  2. Add an Aptfile in the root of the project
heroku buildpacks:add --index 1 heroku-community/apt

Then create an apt file:

# Aptfile
libsqlite3-dev
libsqlite3-0

Enable key repeat in VSCode

Even if your operating system enables key repeat, VSCode will disable it. To turn it on you need to update a default value and restart vscode:

defaults write com.microsoft.VSCode ApplePressAndHoldEnabled -bool false
osascript -e 'tell application "Visual Studio Code" to quit'
osascript -e 'tell application "Visual Studio Code" to activate'

Disable broken VSCode feature

TL;DR in the command palette choose β€œWorkspaces: Configure Workspace Trust" and change β€œStart Prompt" to β€œnever"

VSCode has a nice security feature warning about the risks of unknown file authors, but it is too naive to be useful; it actually has the reverse effect of being insecure (due to the fact most of these folders have been used in vscode for years before this feature was introduced). For example, I have over 200 projects on my machine:

ls ~/dev | wc -l
     213

The start prompt is simply training me to continually click β€œYes I trust this code" over and over again, not only does this have no effect, but actually has a negative effect of making VSCode think I gave careful consideration to the folder, when instead, I was just trying to open my files.

Increase size of ProgressView

You can increase the size of a ProgressView by using the .scaleEffect method:

AsyncImage(url: URL(string: video.poster_url)) { phase in
  if let image = phase.image {
      image
        .resizable()
        .aspectRatio(contentMode: .fill)
  } else if phase.error != nil {
    Color.red
  } else {
    ProgressView()
      .scaleEffect(x: 2, y: 2, anchor: .center)
  }
}
.frame(width: width, height: height)

Split a string in elixir

You can split a string in elixir with String.split

defmodule Phoenix.Repo.Migrations.CreateUsers do
  use Ecto.Migration

  def up do
    execute_sql("""
      create extension citext;

      create table users (
        id bigint generated by default as identity primary key,
        email citext unique not null,
        inserted_at timestamptz not null default now(),
        updated_at timestamptz not null default now()
      );
    """)
  end

  def down do
    execute_sql("""
      drop table users;
      drop extension citext;
    """)
  end
   

  def execute_sql(sql_statements) do
     sql_statements
     |> String.split(";")
     |> Enum.filter(fn s -> String.trim(s) != "" end)
     |> Enum.each(&execute/1)
  end
end

Replace first letter of string

You need to use runes to prevent corrupting unicode values:

package main

import (
	"fmt"
)

func replaceFirstRune(str, replacement string) string {
	return string([]rune(str)[:0]) + replacement + string([]rune(str)[1:])
}

func main() {
	name := "Hats are great!"
	name = replaceFirstRune(name, "C")
	fmt.Println(name)
}

Output:

=> Cats are great!

Just like in ruby, this doesn’t cover multi-byte unicode characters. You still need to do a unicode table lookup:

name = "πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦"
name[0] = "C"
=> "Cβ€πŸ‘©β€πŸ‘§β€πŸ‘¦"
println(replaceFirstRune("πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦", "C"))
=> "Cβ€πŸ‘©β€πŸ‘§β€πŸ‘¦"

You can go step more and replace the man with a woman:

println(replaceFirstRune("πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦", "πŸ‘©"))
=> "πŸ‘©β€πŸ‘©β€πŸ‘§β€πŸ‘¦"

You MUST use a ws protocol in railsπŸ’©actioncable

Rails’ actioncable library is a bit immature compared to other implementations, so there are a lot of rough edges to work around. One of those is the basic createConsumer function.

If your app is running without a DOM (nodejs), the node_module @rails/actioncable is going to fight you.

The rails guides recommend this:

createConsumer('https://ws.example.com/cable')

But that function relies on having a global document that can create tags, which you won’t have in many contexts (node, react-native, etc.)

Also, why does a websocket library depend on HTML anchor tags?

You can work around this limitation by explicitly using a ws or wss protocol:

createConsumer('ws://localhost:3000/cable')

Ruby's ENV::[]= only accepts strings

You cannot set the value of an environment variable to something that is not a string

# THIS DOES NOT WORK
ENV["SKIP_AUTH"] = true
=> `[]=': no implicit conversion of true into String (TypeError)

You can, however, pass an object that implements #to_str

class User < ApplicationRecord
  def to_str
    to_global_id.to_s
  end
end

ENV["user"] = User.first
ENV["user"]
=> "gid://rails-app/User/3f565b9c-0899-49f6-ab20-aa2724235ff5"

Be careful when stubbing ENV in specs:

# ENV could never return a boolean, your tests will lie to you.
RSpec.describe "ENV" do
  before do
    stub_const("ENV", {"SKIP_AUTH" => true})
  end
end

Have OS specific implementations in golang

In go, you can create separate implementations for different operating systems by creating multiple files:

package main

import (
	"play.ground/foo"
)

func main() {
	foo.HelloComputer()
}

-- go.mod --
module play.ground

-- foo/foo_windows.go --
// +build windows

package foo

import "fmt"

func HelloComputer() {
	fmt.Println("Hello windows!")
}

-- foo/foo_linux.go --
// +build !windows

package foo

import "fmt"

func HelloComputer() {
	fmt.Println("Hello not windows!")
}

Pass keyword arguments when using send

Don’t use a hash, just pass send with a comma-separated list of keyword arguments:

class Animal < Struct.new(:name)
  def greet(name:, catch_phrase:)
    puts "Heya #{name}! What's new, #{catch_phrase}?"
  end
end

Animal.new("Rex").send(:greet, name: "Dillon", catch_phrase: "cool cat")
=> "Heya Dillon! What's new, cool cat?"

Sending with a hash will fail:

Animal.new("Rex").send(:greet, {name: "Dillon", catch_phrase: "cool cat"})
=> wrong number of arguments (given 1, expected 0; required keywords: name, catch_phrase) (ArgumentError)

How to use ActiveRecored.pluck without Arel.sql

It is too cumbersome to remember to wrap every string in a class method, so this is a shortcut:

# app/models/application_record.rb

class ApplicationRecord < ActiveRecord::Base
  module ::ActiveRecord::Sanitization::ClassMethods
    define_method(:original_disallow_raw_sql!, instance_method(:disallow_raw_sql!))

    def disallow_raw_sql!(args, permit: connection.column_name_matcher) # :nodoc:
      original_disallow_raw_sql!(args.map { |a| a.is_a?(String) ? Arel.sql(a) : a }, permit: permit)
    end
  end
end

Then you can write sql without the class methods

This can be easier to write:

User.where(payment_due: true)
  .pluck(Arel.sql("coalesce('last_billed_date', 'start_date')"))

Can now just be written as:

User.where(payment_due: true)
  .pluck("coalesce('last_billed_date', 'start_date')")

This can also be useful for .order as well

Rails has helpers for uploading spec fixture files

Instead of writing your own helper method in specs:

module HelpMePlease
  def uploaded_file(path)
    Rack::Test::UploadedFile.new(Rails.root.join("spec/fixtures", path))
  end
end

Rails has a built in helper method:

require "rails_helper"

RSpec.describe HelpNeeded do
  describe "something" do
    it "sends the file" do
      post :change_avatar, params: { avatar: fixture_file_upload("spongebob.png", "image/png") }
    end
  end
end

Add a folder to git with exceptions

Given this folder structure:

app
β”œβ”€β”€ bin
β”‚  └── parse-ansi-codes.rs
β”œβ”€β”€ Cargo.lock
β”œβ”€β”€ Cargo.toml
β”œβ”€β”€ README.md
β”œβ”€β”€ src
β”‚  β”œβ”€β”€ cursor.rs
β”‚  β”œβ”€β”€ lib.rs
β”‚  └── style.rs
β”œβ”€β”€ target
β”‚  └── debug
└── test

I can add the entire app directory to git, while ignoring the bin folder:

$ git add . ':!bin'

rspec should receive thrice

rspec has a #thrice method for testing receive counts:

describe Account do
  context "when opened" do
    it "logger#account_opened was called once" do
      logger = double("logger")
      account = Account.new
      account.logger = logger

      logger.should_receive(:account_opened).thrice

      account.open
      account.open
      account.open
    end
  end
end

Replace multiple characters in ruby strings

Ruby String#tr allows you to replace characters or patterns in strings:

irb(main):001:0> "I love coffee".tr("love", "😍")
=> "I 😍😍😍😍 c😍ff😍😍"

Compare with #gsub:

irb(main):001:0> "I love coffee".gsub("love", "😍")
=> "I 😍 coffee"

If your pattern arg to gsub is only one character consider using #tr, but beware of multi-length from_str arg to #tr

Split text in postgres

You can split text in postgres

select email from users;
bob@example.com
mary@example.com
select split_part(email, '@', 1) from users;
bob
mary
select split_part(email, '@', 2) from users;
@example.com
@example.com

Which can be helpful when sanitizing data:

update users
  set email = split_part(email, '@', 1) || '@example.com';

List cli xcodebuild archives in Xcode's organizer

When using a Makefile to build Xcode applications, it’s nice to have the archives listed in the Organizer window, for easy distribution. This can be accomplished with the -archivePath flag, using a specific directory:

BOB_THE_BUILD_DIR="~/Library/Developer/Xcode/Archives/$(date +%Y-%m-%d)"
ARCHIVE_PATH="$BOB_THE_BUILD_DIR/MyApp-$(date|md5).xcarchive"

xcodebuild -scheme MyApp -workspace "MyApp.xcworkspace" archive -configuration release -archivePath "$ARCHIVE_PATH"

🚯 Prevent development logs from bloating on macOS

I rarely need to refer to development.log or test.log when working on rails applications, but yet I end up keeping weeks or even years of records [gigabytes]. I’m used to working with logrotate, and I wanted to find a similar solution that was preinstalled with macOS. macOS comes preinstalled with a program called newsyslog that can keep file sizes in check. I just created a new file at /etc/newsyslog.d/rails.conf which limits all rail’s log files to just 10MB

# /etc/newsyslog.d/rails.conf
# logfilename                       	                        [owner:group]      mode count size(KB)     when  flags [/pid_file] [sig_num]
/Users//dev//*/log/*.log     		    :staff   644  0     10000  		*     G
# Mono repos
/Users//dev//*/*/log/*.log     	    :staff   644  0     10000  		*     G
# Deeper mono repos
/Users//dev//*/*/*/log/*.log          :staff   644  0     10000  		*     G

You can also perform a dryrun of the config for testing:

sudo newsyslog -v -n -f /etc/newsyslog.d/rails.conf

What directory is the parent of root πŸ‘¨β€πŸ‘¦πŸ“

I learned that in unix, root (e.g. /) is the only directory that is the parent directory of itself.

$ ls -lai / | grep '\./'
                  2 drwxr-xr-x   20 root  wheel   640 Jan  1  2020 ./
                  2 drwxr-xr-x   20 root  wheel   640 Jan  1  2020 ../

In the above example, the files . and .. both have the same i-node: 2

Source: Brian W. Kernighan, & Rob Pike (1984) The UNIX Programming Environment. Prentice-Hall, Inc