GeistHaus
log in · sign up

https://ugur.ozyilmazel.com/feed-en.xml

atom
8 posts
Polling state
Status active
Last polled May 18, 2026 23:48 UTC
Next poll May 20, 2026 00:16 UTC
Poll interval 86400s
ETag W/"6a08a3c6-1d8bb"
Last-Modified Sat, 16 May 2026 17:05:10 GMT

Posts

jc - A JSON Companion
Show full content

Let’s look at a quick example using dig:

$ dig example.com
; <<>> DiG 9.10.6 <<>> example.com
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 42404
;; flags: qr rd ra ad; QUERY: 1, ANSWER: 6, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 512
;; QUESTION SECTION:
;example.com.           IN  A

;; ANSWER SECTION:
example.com.        137 IN  A   23.192.228.80
example.com.        137 IN  A   23.192.228.84
example.com.        137 IN  A   23.220.75.232
example.com.        137 IN  A   23.220.75.245
example.com.        137 IN  A   23.215.0.136
example.com.        137 IN  A   23.215.0.138

;; Query time: 13 msec
;; SERVER: 8.8.8.8#53(8.8.8.8)
;; WHEN: Sun Oct 12 23:45:22 +03 2025
;; MSG SIZE  rcvd: 136

The output from dig contains a lot of data. Alright, let’s convert it to json!

$ dig example.com | jc --dig
[{"id":10302,"opcode":"QUERY","status":"NOERROR","flags":["qr","rd","ra","ad"],"query_num":1,"answer_num":6,"authority_num":0,"additional_num":1,"opt_pseudosection":{"edns":{"version":0,"flags":[],"udp":512}},"question":{"name":"example.com.","class":"IN","type":"A"},"answer":[{"name":"example.com.","class":"IN","type":"A","ttl":108,"data":"23.220.75.245"},{"name":"example.com.","class":"IN","type":"A","ttl":108,"data":"23.192.228.84"},{"name":"example.com.","class":"IN","type":"A","ttl":108,"data":"23.192.228.80"},{"name":"example.com.","class":"IN","type":"A","ttl":108,"data":"23.220.75.232"},{"name":"example.com.","class":"IN","type":"A","ttl":108,"data":"23.215.0.138"},{"name":"example.com.","class":"IN","type":"A","ttl":108,"data":"23.215.0.136"}],"query_time":11,"server":"8.8.8.8#53(8.8.8.8)","when":"Sun Oct 12 22:55:51 +03 2025","rcvd":136,"when_epoch":1760298951,"when_epoch_utc":null}]

Still ugly? Yep!

$ dig example.com | jc --dig | jq

Result:

[
    {
        "id": 3386,
        "opcode": "QUERY",
        "status": "NOERROR",
        "flags": [
            "qr",
            "rd",
            "ra",
            "ad"
        ],
        "query_num": 1,
        "answer_num": 6,
        "authority_num": 0,
        "additional_num": 1,
        "opt_pseudosection": {
            "edns": {
                "version": 0,
                "flags": [],
                "udp": 512
            }
        },
        "question": {
            "name": "example.com.",
            "class": "IN",
            "type": "A"
        },
        "answer": [
            {
                "name": "example.com.",
                "class": "IN",
                "type": "A",
                "ttl": 121,
                "data": "23.215.0.136"
            },
            {
                "name": "example.com.",
                "class": "IN",
                "type": "A",
                "ttl": 121,
                "data": "23.192.228.80"
            },
            {
                "name": "example.com.",
                "class": "IN",
                "type": "A",
                "ttl": 121,
                "data": "23.215.0.138"
            },
            {
                "name": "example.com.",
                "class": "IN",
                "type": "A",
                "ttl": 121,
                "data": "23.220.75.232"
            },
            {
                "name": "example.com.",
                "class": "IN",
                "type": "A",
                "ttl": 121,
                "data": "23.192.228.84"
            },
            {
                "name": "example.com.",
                "class": "IN",
                "type": "A",
                "ttl": 121,
                "data": "23.220.75.245"
            }
        ],
        "query_time": 11,
        "server": "8.8.8.8#53(8.8.8.8)",
        "when": "Sun Oct 12 23:49:27 +03 2025",
        "rcvd": 136,
        "when_epoch": 1760302167,
        "when_epoch_utc": null
    }
]

Fantastic right? You can install jc from brew (macOS):

brew install jc
brew install jq # in case you don’t have jq installed

It’s an open-source tool:

https://github.com/kellyjonbrazil/jc

Examples from jc’s GitHub Page
$ dig example.com | jc --dig | jq -r '.[].answer[].data'

23.192.228.84
23.215.0.136
23.220.75.245
23.192.228.80
23.215.0.138
23.220.75.232

# or magic syntax
$ jc dig example.com | jq -r '.[].answer[].data'

$ git log | jc --git-log | jq
$ ifconfig | jc --ifconfig
$ echo "${JWT_TOKEN}" | jc --jwt
$ echo "/Users/admin/.docker/bin" | jc --path | jq
$ cat foo.yaml | jc --yaml | jq

There are tons of parsers available on GitHub!

Enjoy!

https://ugur.ozyilmazel.com/blog/en/2025/10/12/jc-a-json-companion/
Fix Middleman Live Reload
Show full content

I rarely add posts to my blog — sometimes every 3–4 months, sometimes once a year. Whenever I decide to write a new post, I tell myself

Wait, let’s update the packages first.

Big mistake. It never fails—every single time I sit down to write, I somehow end up battling Ruby versions or package dependencies instead.

A few days ago, I got excited again and decided to write a blog post. I checked GitHub, and dependabot had opened some PRs. I noticed an update for middleman-core. With my fingers crossed, I merged dependabot’s PRs.

Oh my bash! The most useful feature when writing blog posts is live-reload, and guess what? It wasn’t working.

I checked the Firefox console, and webrick was returning a 500 http error. The reason? The incoming HTTP headers had uppercase keys, like Content-Type.

Anyway, I told myself;

Let’s not deal with this now.

So, I disabled the live reload feature, finished my post, and published it.

After that, I turned to the greatest invention of our time—ChatGPT and other LLMs—to explain the issue and ask for help. No luck.

Google? Stack Overflow? Nothing. Of course, it was up to me to fix it.

For the past six years, I’ve been developing with go, so I’ve been following the ruby ecosystem from a distance. To keep my ruby skills from fading, I mostly tinker with Rakefiles. Because of that, I expected this issue to be a bit challenging—but surprisingly, it was an easy fix!.

First, I ran Middleman in verbose mode:

bin/middleman serve --verbose

and here was the error:

[2025-03-11 18:10:15] ERROR Rack::Lint::LintError: uppercase character in header name: Content-Type
    /private/tmp/my_project/vendor/bundle/ruby/3.3.0/gems/rack-3.1.12/lib/rack/lint.rb:717:in `block in check_headers'

Opened the file vendor/bundle/ruby/3.3.0/gems/rack-3.1.12/lib/rack/lint.rb:717 and commented out:

## Header keys must not contain uppercase ASCII characters (A-Z).
# raise LintError, "uppercase character in header name: #{key}" if key =~ /[A-Z]/
# ^ this line

and it worked! Now, it’s time to use some ruby magic to override/implement a custom Rack middleware to that! I undo the comment, and implemented:

# lib/middleware/rack/downcase_headers.rb
module Rack
  class DowncaseHeaders
    def initialize(app)
      @app = app
    end

    def call(env)
      status, headers, body = @app.call(env)
      new_headers = headers.transform_keys(&:downcase)
      [status, new_headers, body]
    end
  end
end

Now, updated my config.rb:

# ...

require_relative 'lib/middleware/rack/downcase_headers'

# ...

configure :development do
  use ::Rack::DowncaseHeaders
  activate :livereload, host: '127.0.0.1'
end

That’s it! Live reload is back in business!

This is just a basic monkey patch. The Middleman team or the live-reload team will probably fix this in the future, but until then, I’ll stick with this workaround.

Files can be found:

The versions of gems are:

  • em-websocket (0.5.3)
  • middleman (4.6.0)
  • middleman-livereload (3.4.7)
  • rack (3.1.12)
  • rack-livereload (0.3.17)
  • webrick (1.9.1)
https://ugur.ozyilmazel.com/blog/en/2025/03/11/fix-middleman-live-reload/
Django Natural Key
Show full content

I think one of the most amazing aspects of Django is the ability to automatically create the database right after defining the model and quickly enter data through the Django admin interface. During development, we add additional fields to the model, and as the model grows, the number of fields we enter through the admin panel also increases.

In certain situations, we use realistic data to decide on the HTML design of the page. For example, while creating a blog application, we enter a few blog entries. These entries have a title and a body, with the body possibly consisting of 4-5 paragraphs.

Sometimes, to avoid creating additional migrations, and since the project hasn’t been deployed yet, we can delete/drop the local development database and migration files and create the migrations from scratch.

In the end, it becomes necessary to repeatedly fill and empty the database. For these situations, we use Django’s fixture1 feature2.

For example, let’s say we have a model named Category:

import uuid

from django.db import models


class Category(models.Model):
    id = models.UUIDField(
        primary_key=True,
        default=uuid.uuid4,
        editable=False,
    )
    title = models.CharField(max_length=255)

    def __str__(self):
        return self.title

Let’s say we have entered some data and we want to create a fixture for this model:

python manage.py dumpdata coders.category  # coders is the name of app
                                           # category is the name of model

Django outputs the dump result as json to console:

[
    {
        "model": "coders.category",
        "pk": "0d40f3ad-a500-4717-a0c4-cbaf880306df",
        "fields": {
            "title": "C++"
        }
    },
    {
        "model": "coders.category",
        "pk": "1999fd35-7513-4b5d-add1-11db4d073cf3",
        "fields": {
            "title": "PHP"
        }
    },
    {
        "model": "coders.category",
        "pk": "24786be1-7c4e-4923-baf6-54f61fe70976",
        "fields": {
            "title": "Bash"
        }
    }
]

Let’s say we have a Post model and a category field that is linked to the Category model with a ForeignKey:

import uuid

from django.db import models


class Post(models.Model):
    id = models.UUIDField(
        primary_key=True,
        default=uuid.uuid4,
        editable=False,
    )
    category = models.ForeignKey(
        to='Category',
        on_delete=models.CASCADE,
        related_name='posts',
    )
    title = models.CharField(max_length=255)

    def __str__(self):
        return self.title

Similarly, let’s serialize it to JSON using dumpdata:

python manage.py dumpdata coders.post

Output:

[
    {
        "model": "coders.post",
        "pk": "19b20f6b-b352-4152-8889-ec6c9118e28c",
        "fields": {
            "category": "1999fd35-7513-4b5d-add1-11db4d073cf3",
            "title": "Modern PHP Frameworks"
        }
    },
    {
        "model": "coders.post",
        "pk": "40cfc47f-bae2-4dff-a812-9fafea349799",
        "fields": {
            "category": "24786be1-7c4e-4923-baf6-54f61fe70976",
            "title": "Bash Scripting Essentials"
        }
    },
    {
        "model": "coders.post",
        "pk": "4cb31890-3c7b-461d-b87e-98131d0ee150",
        "fields": {
            "category": "0d40f3ad-a500-4717-a0c4-cbaf880306df",
            "title": "Templates and Generic Programming in C++"
        }
    }
]

When we look at the output, we see that the pk as primary key for Post model and the category field in the foreign key relationship contain the pk value as Category model’s pk.

The question is; when restoring this dump data, what order should we follow? Should we load the dump of the Category model first and then the dump of the Post model? Or is it the other way around?

What if the Post model has other Foreign Keys or even Many to Many relationships? In what order will we restore them? And will these restore operations be idempotent?

Natural Keys to the Rescue!

In fact, the natural key strategy has been part of Django since the beginning. If you have used Django’s built-in User model and serialized with dumpdata using the --natural-primary and --natural-foreign arguments, you might have noticed an interesting field in the output:

[
    {
        "model": "auth.user",
        "fields": {
            "password": "pbkdf2_sha256$720000$VrtuqbddGBXZxssR5dszGV$JgsU4a8sQnTGQ8RlND36CeXuTlZHugN3nID5wxNF+Nw=",
            "last_login": "2024-05-19T18:08:04.481Z",
            "is_superuser": true,
            "username": "vigo",
            "first_name": "Uğur",
            "last_name": "Özyılmazel",
            "email": "vigo@******",
            "is_staff": true,
            "is_active": true,
            "date_joined": "2024-05-18T20:44:02.729Z",
            "groups": [

            ],
            "user_permissions": [

            ]
        }
    },
    {
        "model": "auth.user",
        "fields": {
            "password": "pbkdf2_sha256$720000$VrtuqbddGBXZxssR5dszGV$JgsU4a8sQnTGQ8RlND36CeXuTlZHugN3nID5wxNF+Nw=",
            "last_login": "2024-05-18T20:45:04.615Z",
            "is_superuser": true,
            "username": "turbo",
            "first_name": "Tunç",
            "last_name": "Dindaş",
            "email": "turbo@******"",
            "is_staff": false,
            "is_active": true,
            "date_joined": "2024-05-18T20:44:02.729Z",
            "groups": [

            ],
            "user_permissions": [

            ]
        }
    }
]

Did you see a field named pk ? No. Now, let’s add an author field to the Post model and take another dump:

import uuid

from django.conf import settings


class Post(models.Model):
    id = models.UUIDField(
        primary_key=True,
        default=uuid.uuid4,
        editable=False,
    )
    category = models.ForeignKey(
        to='Category',
        on_delete=models.CASCADE,
        related_name='posts',
    )
    title = models.CharField(max_length=255)
    author = models.ForeignKey(
        to=settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
        related_name='posts',
    )

    def __str__(self):
        return self.title

A snippet from the data we dumped:

[
    {
        "model": "coders.post",
        "pk": "19b20f6b-b352-4152-8889-ec6c9118e28c",
        "fields": {
            "category": "1999fd35-7513-4b5d-add1-11db4d073cf3",
            "title": "Modern PHP Frameworks",
            "author": 5
        }
    },
    {
        "model": "coders.post",
        "pk": "1090da3a-af48-47dc-8041-c282b84b3d04",
        "fields": {
            "category": "f7d1ff24-c436-4ef5-bfb5-c1e8bcb64942",
            "title": "Text Processing with Perl",
            "author": 6
        }
    },
    {
        "model": "coders.post",
        "pk": "17b0eea1-0ce5-4bc2-b4c2-13e1fd073516",
        "fields": {
            "category": "ea006dc9-7267-4d03-9541-37d42cdbefc1",
            "title": "Introduction to JavaScript ES6",
            "author": 4
        }
    }
]

Now, the question is: when restoring the author and category fields related to this model with loaddata, what order should we follow? The record with user ID 4 (author) should be inserted into the user table with ID 4 during the restore process. Who guarantees this?

Let’s find out the unique fields of User model:

[
    f.name
    for f in User._meta.get_fields()
        if getattr(f, 'unique', None) and f.get_internal_type() != 'AutoField'
]
['username']

Now let’s find out the username for user ids 4,5,6:

User.objects.values_list('id', 'username').filter(id__in=[4,5,6])
<QuerySet [(4, 'flatliners'), (5, 'ezelozy'), (6, 'yesimfo')]>

Why not use username instead of id in fixture? There can be only one flatliners or ezelozy or yesimfo right? How do we accomplish that? To avoid such primary key confusions, Django provides us with an excellent model instance method: natural_key. If we look at django/contrib/auth/base_user.py:

class AbstractBaseUser(models.Model):
    # fields, properties
    #
    def natural_key(self):
        return (self.get_username(),)

So, if we ask, what is the natural key for user ID 4?

User.objects.get(id=4).natural_key()
('flatliners',)

In the same file:

class BaseUserManager(models.Manager):
    # methods, etc...
    #
    def get_by_natural_key(self, username):
        return self.get(**{self.model.USERNAME_FIELD: username})

In the help text of dumpdata there are two options:

python manage.py dumpdata --help

  --natural-foreign     Use natural foreign keys if they are available.
  --natural-primary     Use natural primary keys if they are available.

If the model manager has get_by_natural_key method use it and generate serialization with using natural keys instead of id values. So, if we update our models:

class CategoryManager(models.Manager):
    def get_by_natural_key(self, title):
        return self.get(title=title)

class Category(models.Model):
    id = models.UUIDField(
        primary_key=True,
        default=uuid.uuid4,
        editable=False,
    )
    title = models.CharField(max_length=255, unique=True)

    objects = CategoryManager()

    def __str__(self):
        return self.title

    def natural_key(self):
        return (self.title,)


class PostManager(models.Manager):
    def get_by_natural_key(self, title, category_nk):
        category = Category.objects.get_by_natural_key(*category_nk)
        return self.get(title=title, category=category)

class Post(models.Model):
    id = models.UUIDField(
        primary_key=True,
        default=uuid.uuid4,
        editable=False,
    )
    category = models.ForeignKey(
        to='Category',
        on_delete=models.CASCADE,
        related_name='posts',
    )
    title = models.CharField(max_length=255)
    author = models.ForeignKey(
        to=settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
        related_name='posts',
    )

    objects = PostManager()

    def __str__(self):
        return self.title

    def natural_key(self):
        return (self.title, self.category.natural_key())
Serialization / Deserialization

natural_key is used to define a key (more humane) instead of ID presentation. This method should return a tuple that uniquely identifies an instance of your model using fields other than the primary key. It is used in serialization process.

get_by_natural_key is used to retrieve an instance of your model using the natural key fields which should be implemented in model’s manager. It is used in deserialization process.

In our example, author field is a foreign key to User model and User model is already shipped with this strategy. So, if we call the dumpdata:

python manage.py dumpdata coders.post --indent 4 --natural-foreign --natural-primary

The result looks like this:

[
    {
        "model": "coders.post",
        "fields": {
            "category": [
                "JavaScript"
            ],
            "title": "Asynchronous JavaScript",
            "author": [
                "flatliners"
            ]
        }
    },
    {
        "model": "coders.post",
        "fields": {
            "category": [
                "Perl"
            ],
            "title": "Text Processing with Perl",
            "author": [
                "yesimfo"
            ]
        }
    },
    {
        "model": "coders.post",
        "fields": {
            "category": [
                "PHP"
            ],
            "title": "Modern PHP Frameworks",
            "author": [
                "ezelozy"
            ]
        }
    }
]

Now, when Django loads the fixture with loaddata, it uses natural keys instead of ID values to find the category and author. Pretty amazing, right?

We can also arrange dependencies during serialization. If we look at Django’s documentation:

class Book(models.Model):
    name = models.CharField(max_length=100)
    author = models.ForeignKey(Person, on_delete=models.CASCADE)

    def natural_key(self):
        return (self.name,) + self.author.natural_key()

Book’s natural key is dependent on Person’s natural key. Therefore Person model should be serialized before the Book model. All we need is to add natural_key.dependencies declaration to Book model:

class Book(models.Model):
    name = models.CharField(max_length=100)
    author = models.ForeignKey(Person, on_delete=models.CASCADE)

    def natural_key(self):
        return (self.name,) + self.author.natural_key()

    natural_key.dependencies = ['example_app.person']

The serialization order will be as follows: first, the Person model, then the Book model will be serialized. Thanks to natural_key.dependencies

Finally, if you frequently need fixtures and perform load/dump operations like I do, you can confidently transfer data from one place to another without struggling with integrity errors. You can prepare fresh initial data for your project, and you can even quickly write and load these fixtures manually.


Resources
https://ugur.ozyilmazel.com/blog/en/2024/05/21/django-natural-key/
Brew formula, small deprecation
Show full content

I moved from go 1.16 to go 1.19. I had to come out with a new version. I upgraded my custom brew package as I always do; fix the url and sha fields in the Formula, fix the basic test under test area.

I pushed my changes and started to wait github actions’ checks. Checks are failed, due to brew’s formula style guide:

==> brew style vigo/statoo
==> FAILED
Full style vigo/statoo output
  Formula/statoo.rb:18:5: C: Use generate_completions_from_executable DSL instead of (bash_completion/"statoo").write #{bin}/statoo bash-completion.
      (bash_completion/"statoo").write `#{bin}/statoo bash-completion`
      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

  2 files inspected, 1 offense detected
  Error: Use `generate_completions_from_executable` DSL instead of `(bash_completion/"statoo").write `#{bin}/statoo bash-completion``.

In the install part of the formula; bash completion related setup was written as shown below:

def install
  system "go", "build", *std_go_args
  (bash_completion/"statoo").write `#{bin}/statoo bash-completion`
end

Checker was telling me that, I should use generate_completions_from_executable DSL instead of old one. Well, what was that? when was changed? Let’s fix this :) First, let’s read the docs, then change the code to:

def install
  system "go", "build", *std_go_args
  generate_completions_from_executable(bin/"statoo", "bash-completion", "completions", shells: [:bash])
end

Break down of args:

  • bin/"statoo" is the executable path
  • bash-completion is an arg for statoo executable, generates bash related completion code.
  • completions is the base_name
  • Completion code is compatible with bash only, therefor I set the shel :bash only.

I spent an hour to fix and understand the problem and solution. I couldn’t find any examples or documentation about this new formula style change… This is why I’m writing this blog post so that will be useful to other brewers :)

Here is the function signature of generate_completions_from_executable:

generate_completions_from_executable(
  *commands, 
  base_name: name, 
  shells: [:bash, :zsh, :fish], 
  shell_parameter_format: nil
) ⇒ void

I hope this post will help, happy brew packaging! You can check project and custom brew package here:

https://ugur.ozyilmazel.com/blog/en/2022/10/08/brew-formula-small-deprecation/
Store websites as pdf
Show full content

I subscribe to many newsletters about programming. I have a git repository where I keep sample codes, small snippets from the articles I read. Sometimes I want to keep the entire article for later usage.

I was keeping bookmarks… Tons of links that I’ll read one day… Bookmarking is easy but requires maintenance, synchronization between devices, backup etc… One day I realized that websites don’t live forever. Domain name can expire, the author can close/shutdown the site or site may fall into black hole somehow :)

I should have found a different method. I realized that Safari (macOS) has a feature that I can save entire page as webarchive format. I made some small automation tools to save page as webarchive than create gzip’ped tar files and store them under classified folders in a git repo.

First, I was using GitHub. One day, I’ve received an error while pushing the files to remote repository. I have reached the maximum storage limit. Turns out the maximum repository size limit was 1 Gigabyte.

I quickly transferred my repository to GitLab. I was only storing compressed webarchive files and sometimes png screenshot files. GitLab is like a bottomless pit, feels like no limits!

What? Size LFS Storage 4.4 GB Repository 2.0 GB 6.4 GB # of files 5178

Normally, webarchive files are default/native macOS format and can be previewed via built-in QuickLook feature. Somehow webarchive support is broken by itself. QuickLook previews were working fine but opening files with Safari was total disaster.

At least, It was still possible to open webarchive files with TextEdit that shipped with macOS. Most of the styles were broken but text and the code part was still readable.

Few days ago, I realized that Safari has PDF export feature under File > Export as PDF.... Feature was working fine and were saving the whole website as a one single and large pdf file.

Small clip from Safari screenshot

Safari > File > Export as PDF

There was one little issue: the file size. How can I reduce the pdf size?

brew install ghostscript

gs -sDEVICE=pdfwrite \
    -dNOPAUSE -dQUIET -dBATCH \
    -dPDFSETTINGS=/screen \
    -dCompatibilityLevel=1.4 \
    -sOutputFile="out-reduced.pdf" "input-large.pdf"

You can read more about -dPDFSETTINGS here. screen is like; selects low-resolution output similar to the Acrobat Distiller (up to version X) Screen Optimized setting. I’ve tried every output options, screen seems the smallest one:

Format Size webarchive 6.2 MB Export as PDF 2.6 MB /prepress 1 MB /screen 383 KB /printer 821 KB /ebook 611 KB

After compressing with tar+gz, 383 KB reduced to 291 KB. File size is reduced by approximately 20 times (6.2 MB -> 291 KB). Not that bad ha? I wish I knew these methods/techniques earlier. Thanks to superuser answer!

I just modifed the example function:

compress_pdf() {
    if [[ "${1}" == "-h" || "${1}" == "--help" ]]; then
        echo "compress_pdf: [input file] [output file] [screen|ebook|printer|prepress]"
        echo
        echo "Requires: ghostscript (brew install ghostscript)"
        echo
        echo "Usage:"
        echo -e "\tcompress_pdf large.pdf small.pdf  # default is: screen"
        echo -e "\tcompress_pdf large.pdf small.pdf ebook"
        echo
        return
    fi

    if [[ -z "$(command -v gs)" ]]; then
        echo "You need ghostscript: brew install ghostscript"
        return
    fi

    echo "pdf comression started" &&
    gs -sDEVICE=pdfwrite \
        -dNOPAUSE -dQUIET -dBATCH \
        -dPDFSETTINGS=/"${3:-screen}" \
        -dCompatibilityLevel=1.4 \
        -sOutputFile="${2}" "${1}" &&
    echo "pdf comression completed"
}
https://ugur.ozyilmazel.com/blog/en/2022/10/02/store-websites-as-pdf/
Hello world...
Show full content

It’s always on my mind to write what I learned, for some reason I can’t find enough time. I plan to share my programming experiences over time. Let’s see how successful I can be?

https://ugur.ozyilmazel.com/blog/en/2022/08/13/hello-world/
Basic phonebook via BASH
Show full content

Yes, you can implement super basic phonebook application via shell script. Create a file called: phonebook

# phonebook

#!/usr/bin/env bash

# name phone-number
# name phone-number
# name phone-number
# name phone-number

grep -i $* <<EOF
vigo  0123-456-78-99
jack  0123-456-78-99
max   0123-456-78-99
turbo 0123-456-78-99
EOF

then make it executable: chmod +x phonebook. Now make query!

$ ./phonebook vigo
vigo 0123-456-78-99

Thanks to Carl Albing for Great Bash course at Safaribooks

https://ugur.ozyilmazel.com/blog/en/2019/11/29/basic-phonebook-via-bash/
Hello world
Show full content

Hello friends! I’ve been thinking about this for a long time. I’d like to share my blog both in Turkish and English. I’m planning to write more about Ruby, Python, Golang, Bash and OS X related topics.

I hope I’ll make it 8)

https://ugur.ozyilmazel.com/blog/en/2016/07/24/hello-world/