<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Perdi — Blog</title><description>A developer blog by Perdi: practical articles on programming, Linux, and software engineering, plus a few projects.</description><link>https://portofolio-xi-nine-80.vercel.app/</link><language>en</language><atom:link href="https://portofolio-xi-nine-80.vercel.app/rss.xml" rel="self" type="application/rss+xml"/><lastBuildDate>Tue, 15 Sep 2026 00:00:00 GMT</lastBuildDate><item><title>Habits for Debugging Faster</title><link>https://portofolio-xi-nine-80.vercel.app/blog/debugging-habits/</link><guid isPermaLink="true">https://portofolio-xi-nine-80.vercel.app/blog/debugging-habits/</guid><description>Practical debugging habits that save time: reproduce first, read the error, test one hypothesis at a time, bisect, log with context, and write a failing test.</description><pubDate>Tue, 15 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Debugging speed is mostly about habits, not cleverness. These are the ones that have saved me the most time, roughly in the order I use them.&lt;/p&gt;
&lt;h2&gt;Make it fail on demand&lt;/h2&gt;
&lt;p&gt;Before changing anything, find a way to trigger the bug reliably. A bug you can reproduce in ten seconds is half fixed. A bug you can only see “sometimes” is a guessing game.&lt;/p&gt;
&lt;p&gt;Then shrink it. Remove inputs, steps and features until the smallest case that still fails remains. Along the way you usually discover what the bug does not depend on.&lt;/p&gt;
&lt;h2&gt;Read the actual error&lt;/h2&gt;
&lt;p&gt;It sounds obvious, but I still catch myself skimming. Read the message in full, then read the stack trace from the top and find the first frame that is your code. The message often names the exact variable or line, and the trace shows how you got there.&lt;/p&gt;
&lt;h2&gt;One hypothesis at a time&lt;/h2&gt;
&lt;p&gt;Guess a cause, decide what you would observe if you were right, and check. Change one thing per experiment. If you change three things and the bug disappears, you have learned nothing about which one mattered.&lt;/p&gt;
&lt;p&gt;Writing the hypothesis down helps. If I cannot explain the problem in a couple of sentences, to a colleague or to a rubber duck, I probably do not understand it yet. Often the explanation itself reveals the mistake.&lt;/p&gt;
&lt;h2&gt;Bisect everything&lt;/h2&gt;
&lt;p&gt;Binary search is the best debugging tool there is. Use it on history and on data.&lt;/p&gt;
&lt;p&gt;For history, &lt;code&gt;git bisect&lt;/code&gt; finds the commit that introduced a bug in a handful of steps. If you have a command that exits non-zero on failure, it can run unattended.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git bisect start
git bisect bad HEAD
git bisect good v1.4.0
git bisect run npm test
git bisect reset
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Without &lt;code&gt;run&lt;/code&gt;, git checks out a middle commit and you mark it with &lt;code&gt;git bisect good&lt;/code&gt; or &lt;code&gt;git bisect bad&lt;/code&gt; yourself. Bisecting works far better when commits are small and focused, which is one more reason for &lt;a href=&quot;https://portofolio-xi-nine-80.vercel.app/blog/writing-good-commit-messages/&quot;&gt;writing good commit messages&lt;/a&gt; and keeping changes tidy.&lt;/p&gt;
&lt;p&gt;For data, halve the input. If a 10,000-line file breaks the parser, try the first half, then the half of that, until you reach the offending line.&lt;/p&gt;
&lt;h2&gt;Log with context&lt;/h2&gt;
&lt;p&gt;A bare &lt;code&gt;console.log(&quot;here&quot;)&lt;/code&gt; tells you little. Log named values, and wrap them in an object so the names are printed alongside them.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;function applyDiscount(cart, coupon) {
  console.log(&quot;applyDiscount:input&quot;, { cartId: cart.id, total: cart.total, coupon });
  const rate = coupon?.rate ?? 0;
  const result = cart.total * (1 - rate);
  console.log(&quot;applyDiscount:output&quot;, { rate, result });
  return result;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Log at boundaries: what went in, what came out. Remove the logs when you are done.&lt;/p&gt;
&lt;h2&gt;Use the debugger for state you cannot reason about&lt;/h2&gt;
&lt;p&gt;When the logic is too tangled to simulate in your head, set a breakpoint and inspect. A debugger lets you see every variable at once, step line by line, and evaluate expressions without editing code. In Node, &lt;code&gt;node --inspect-brk app.js&lt;/code&gt; lets Chrome DevTools or your editor attach.&lt;/p&gt;
&lt;h2&gt;Write the failing test first&lt;/h2&gt;
&lt;p&gt;Once you understand the bug, capture it in a test before fixing it. Watch the test fail, apply the fix, and watch it pass. That proves the test actually detects the problem, and it keeps the bug from coming back.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-diff&quot;&gt; describe(&quot;applyDiscount&quot;, () =&amp;gt; {
   it(&quot;applies a percentage coupon&quot;, () =&amp;gt; {
     expect(applyDiscount({ total: 100 }, { rate: 0.1 })).toBe(90);
   });
+  it(&quot;ignores an expired coupon&quot;, () =&amp;gt; {
+    expect(applyDiscount({ total: 100 }, { rate: 0.1, expired: true })).toBe(100);
+  });
 });
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Take a break&lt;/h2&gt;
&lt;p&gt;If you have been stuck for an hour, walk away. Fresh eyes spot what tired ones skip, and many of my hardest bugs have been solved in the shower or on a short walk.&lt;/p&gt;
&lt;h2&gt;What to do next&lt;/h2&gt;
&lt;p&gt;Pick one habit and apply it to your next bug. For me, forcing myself to write down a hypothesis before touching code made the biggest difference. If your bug involves promises, &lt;a href=&quot;https://portofolio-xi-nine-80.vercel.app/blog/async-await-in-javascript/&quot;&gt;async/await in JavaScript without the confusion&lt;/a&gt; covers a few common traps worth checking first.&lt;/p&gt;
</content:encoded><category>Tips &amp; Tricks</category><category>debugging</category><category>workflow</category><category>productivity</category><category>testing</category><author>perdidev@gmail.com (Perdi)</author></item><item><title>How to Run Effective Code Reviews</title><link>https://portofolio-xi-nine-80.vercel.app/blog/effective-code-reviews/</link><guid isPermaLink="true">https://portofolio-xi-nine-80.vercel.app/blog/effective-code-reviews/</guid><description>A practical guide to code review from both sides: what authors should prepare, how reviewers should comment, and how to tell if your process is healthy.</description><pubDate>Tue, 24 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;What a review is for&lt;/h2&gt;
&lt;p&gt;I think of code review as a conversation with three goals: catch correctness problems, check that the design fits the codebase, and spread knowledge so that more than one person understands each change.&lt;/p&gt;
&lt;p&gt;Style policing is not on that list. Arguments about quotes, import order, or line length are a waste of human attention. Let a formatter and a linter settle them in CI, and keep reviewer energy for things a tool cannot judge.&lt;/p&gt;
&lt;h2&gt;The author’s side&lt;/h2&gt;
&lt;p&gt;Most of what makes a review good happens before the reviewer opens the page.&lt;/p&gt;
&lt;h3&gt;Keep pull requests small&lt;/h3&gt;
&lt;p&gt;A change that fits on one screen gets read. A 1,500-line change gets skimmed and approved. In my blog starter, small PRs worked well: each one did a single thing, and reviews usually finished the same day.&lt;/p&gt;
&lt;h3&gt;Explain the change and review it yourself&lt;/h3&gt;
&lt;p&gt;Link the issue, describe the approach, and say what you are unsure about. A template makes that the default:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-md&quot;&gt;## What and why

&amp;lt;!-- One or two sentences. Link the issue: Closes #123 --&amp;gt;

## How to test

&amp;lt;!-- Commands or steps a reviewer can follow --&amp;gt;

## Notes for the reviewer

&amp;lt;!-- Risky areas, trade-offs, things you want a second opinion on --&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Before requesting review, read your own diff as if someone else wrote it. This command gives a quick overview of what the PR touches:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git diff main...HEAD --stat
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I regularly find leftover debug lines and unrelated edits this way. Clean commit messages help here too, see &lt;a href=&quot;https://portofolio-xi-nine-80.vercel.app/blog/writing-good-commit-messages/&quot;&gt;Writing Good Commit Messages&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;The reviewer’s side&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Respond within one working day, even if only to say when you will get to it.&lt;/li&gt;
&lt;li&gt;Read the description first, then the tests, then the diff. Tests show intended behavior.&lt;/li&gt;
&lt;li&gt;Ask questions instead of giving orders: “What happens if the list is empty?” invites a better answer than “Handle empty lists.”&lt;/li&gt;
&lt;li&gt;Praise good decisions. Review is not only a hunt for faults.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Label your comments&lt;/h3&gt;
&lt;p&gt;Prefixes remove ambiguity about what is required:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;blocking:&lt;/code&gt; must be fixed before merge.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;question:&lt;/code&gt; I do not understand yet, and I may be wrong.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nit:&lt;/code&gt; a minor preference, take it or leave it.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A suggestion can be as small as a code change in the comment:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-diff&quot;&gt;- const items = posts.filter((p) =&amp;gt; p.data.draft === false);
+ const items = posts.filter((p) =&amp;gt; !p.data.draft);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Something like “nit: &lt;code&gt;!p.data.draft&lt;/code&gt; also covers a missing field” tells the author this is optional.&lt;/p&gt;
&lt;h2&gt;What I look for&lt;/h2&gt;
&lt;p&gt;A short checklist keeps me consistent:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Behavior:&lt;/strong&gt; does it do what the description says?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Edge cases:&lt;/strong&gt; empty input, large input, failures, concurrency.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tests:&lt;/strong&gt; do they fail without the change?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Naming:&lt;/strong&gt; will the next reader understand it?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Security:&lt;/strong&gt; untrusted input, secrets, permissions.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Performance:&lt;/strong&gt; anything accidentally quadratic in a hot path?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Docs:&lt;/strong&gt; does a README or comment need an update?&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;When you disagree&lt;/h2&gt;
&lt;p&gt;Separate “must fix” from “preference”. If it is a preference, say so and let the author decide. If it is a real disagreement, move to a call or a chat rather than a fifth round of comments, then write the outcome back in the PR so the decision is not lost. When two reasonable designs remain, the author who will maintain the code usually gets the final say.&lt;/p&gt;
&lt;h2&gt;Measuring review health&lt;/h2&gt;
&lt;p&gt;I only track one number: time to first review. Long waits cause large PRs, context switching, and resentment. Review size and the number of back-and-forth rounds are useful secondary signals. Treat them as conversation starters for the team, never as individual performance metrics.&lt;/p&gt;
&lt;h2&gt;Next steps&lt;/h2&gt;
&lt;p&gt;Add a pull request template to your repository this week, and agree on comment labels with your team. When the next review stalls, check whether the PR is simply too big. Better debugging also makes reviews easier, so &lt;a href=&quot;https://portofolio-xi-nine-80.vercel.app/blog/debugging-habits/&quot;&gt;debugging habits&lt;/a&gt; is a good companion read.&lt;/p&gt;
</content:encoded><category>Software Engineering</category><category>code-review</category><category>collaboration</category><category>workflow</category><category>git</category><author>perdidev@gmail.com (Perdi)</author></item><item><title>Big-O Notation Without the Anxiety</title><link>https://portofolio-xi-nine-80.vercel.app/blog/big-o-without-the-anxiety/</link><guid isPermaLink="true">https://portofolio-xi-nine-80.vercel.app/blog/big-o-without-the-anxiety/</guid><description>A calm, practical tour of Big-O: the few complexity classes you actually meet, JavaScript examples, a Set optimisation, and when to just measure.</description><pubDate>Tue, 13 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Big-O has a reputation as interview trivia, but the idea is small and useful. You do not need the maths; you need a feel for how code slows down as data grows.&lt;/p&gt;
&lt;h2&gt;What Big-O answers&lt;/h2&gt;
&lt;p&gt;Big-O describes how the amount of work grows as the input grows. It does not tell you how many milliseconds something takes. It tells you what happens when &lt;code&gt;n&lt;/code&gt; goes from a thousand to a million.&lt;/p&gt;
&lt;h2&gt;The classes you actually meet&lt;/h2&gt;
&lt;p&gt;Five cover almost everything in day-to-day web code.&lt;/p&gt;
&lt;h3&gt;O(1) and O(log n)&lt;/h3&gt;
&lt;p&gt;Constant time means the work does not depend on input size. Reading an array index is the typical example.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const firstItem = items[0];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Logarithmic time means each step discards half the remaining data. Binary search on a sorted array is the standard case.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;function binarySearch(sorted, target) {
  let lo = 0;
  let hi = sorted.length - 1;
  while (lo &amp;lt;= hi) {
    const mid = Math.floor((lo + hi) / 2);
    if (sorted[mid] === target) return mid;
    if (sorted[mid] &amp;lt; target) lo = mid + 1;
    else hi = mid - 1;
  }
  return -1;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A million items need only about twenty steps.&lt;/p&gt;
&lt;h3&gt;O(n), O(n log n) and O(n^2)&lt;/h3&gt;
&lt;p&gt;Linear time touches each item once. &lt;code&gt;find&lt;/code&gt; is O(n) in the worst case, because it may scan the whole array.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const user = users.find((u) =&amp;gt; u.id === id);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Good general-purpose sorting is O(n log n), which is what &lt;code&gt;Array.prototype.sort&lt;/code&gt; gives you in practice.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const sorted = [...numbers].sort((a, b) =&amp;gt; a - b);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Quadratic time usually means a loop inside a loop over the same data.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;for (let i = 0; i &amp;lt; items.length; i++) {
  for (let j = i + 1; j &amp;lt; items.length; j++) {
    if (items[i] === items[j]) console.log(&quot;duplicate&quot;, items[i]);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As &lt;code&gt;n&lt;/code&gt; grows, the curves diverge dramatically, and the quadratic one is the one that eventually hurts.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://portofolio-xi-nine-80.vercel.app/_astro/big-o-growth.BdBnBT_L_Zbzh9y.webp&quot; alt=&quot;Line chart comparing how the number of operations grows with input size for O(1), O(log n), O(n), O(n log n) and O(n squared)&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Dropping constants and lower terms&lt;/h2&gt;
&lt;p&gt;If a function does &lt;code&gt;3n + 10&lt;/code&gt; operations, we call it O(n). Constants and smaller terms stop mattering as &lt;code&gt;n&lt;/code&gt; grows, so Big-O keeps only the dominant one. Two sequential loops are still O(n), while a loop nested in a loop is O(n^2).&lt;/p&gt;
&lt;h2&gt;A practical fix: includes versus Set&lt;/h2&gt;
&lt;p&gt;This is the most common real-world win I see. Calling &lt;code&gt;includes&lt;/code&gt; inside a loop scans an array each time.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// O(n^2): includes scans b for every item in a
function commonSlow(a, b) {
  return a.filter((x) =&amp;gt; b.includes(x));
}

// O(n): building the Set is linear, each has() is constant on average
function common(a, b) {
  const lookup = new Set(b);
  return a.filter((x) =&amp;gt; lookup.has(x));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The trade is a bit of extra memory for a much faster lookup.&lt;/p&gt;
&lt;h2&gt;For small n, measure&lt;/h2&gt;
&lt;p&gt;Big-O ignores constants, and for small inputs constants dominate. A simple array scan over 20 items can beat a &lt;code&gt;Set&lt;/code&gt; because of lower overhead. Before optimising, measure with realistic data.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const a = Array.from({ length: 20000 }, (_, i) =&amp;gt; i);
const b = Array.from({ length: 20000 }, (_, i) =&amp;gt; i * 2);

console.time(&quot;includes&quot;);
commonSlow(a, b);
console.timeEnd(&quot;includes&quot;);

console.time(&quot;set&quot;);
common(a, b);
console.timeEnd(&quot;set&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;performance.now()&lt;/code&gt; works too if you want the numbers in code. Measuring instead of guessing is also the heart of &lt;a href=&quot;https://portofolio-xi-nine-80.vercel.app/blog/debugging-habits/&quot;&gt;habits for debugging faster&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Space complexity in a paragraph&lt;/h2&gt;
&lt;p&gt;Space complexity asks the same question about memory. &lt;code&gt;commonSlow&lt;/code&gt; uses O(1) extra space apart from its result, while &lt;code&gt;common&lt;/code&gt; uses O(n) for the &lt;code&gt;Set&lt;/code&gt;. Faster code often costs memory, so it is worth knowing which resource is scarcer in your situation.&lt;/p&gt;
&lt;h2&gt;What to do next&lt;/h2&gt;
&lt;p&gt;Pick a hot path in your own code, such as a loop that calls &lt;code&gt;includes&lt;/code&gt;, &lt;code&gt;indexOf&lt;/code&gt; or &lt;code&gt;find&lt;/code&gt;, and ask what happens at ten times the data. If the answer is “much worse”, try a &lt;code&gt;Set&lt;/code&gt; or &lt;code&gt;Map&lt;/code&gt;, then measure before and after.&lt;/p&gt;
</content:encoded><category>Programming</category><category>algorithms</category><category>performance</category><category>javascript</category><author>perdidev@gmail.com (Perdi)</author></item><item><title>Shell Aliases and Functions That Save Time</title><link>https://portofolio-xi-nine-80.vercel.app/blog/shell-aliases-and-functions/</link><guid isPermaLink="true">https://portofolio-xi-nine-80.vercel.app/blog/shell-aliases-and-functions/</guid><description>Practical bash aliases and functions I use daily, from git shortcuts to mkcd and extract, and how to keep them tidy, safe, and portable.</description><pubDate>Tue, 18 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;If you type the same long command more than a few times a day, the shell can do the typing for you. Aliases and functions are the two tools for this, and picking the right one avoids a lot of confusion. Here is what I have settled on after years of tweaking.&lt;/p&gt;
&lt;h2&gt;Alias or function?&lt;/h2&gt;
&lt;p&gt;An alias is plain text substitution at the start of a command line. It is perfect for shortening a command with fixed options. The limitation is that arguments are only appended at the end, so an alias cannot place them in the middle or run several steps with logic.&lt;/p&gt;
&lt;p&gt;A function is a small shell script living in your session. It takes positional arguments (&lt;code&gt;$1&lt;/code&gt;, &lt;code&gt;$2&lt;/code&gt;, &lt;code&gt;&quot;$@&quot;&lt;/code&gt;), can use conditionals, and can run multiple commands. My rule: start with an alias, and switch to a function the moment you need an argument anywhere but the end.&lt;/p&gt;
&lt;h2&gt;Aliases worth having&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# git
alias gs=&apos;git status -sb&apos;
alias gl=&apos;git log --oneline --graph --decorate -20&apos;
alias gd=&apos;git diff&apos;

# safer file operations
alias rm=&apos;rm -i&apos;
alias cp=&apos;cp -i&apos;
alias mv=&apos;mv -i&apos;

# navigation and listing
alias ll=&apos;ls -lAh&apos;
alias ..=&apos;cd ..&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;On the &lt;code&gt;rm -i&lt;/code&gt; line: it prompts before every deletion, which saves you once and annoys you often. Many people prefer a trash tool such as &lt;code&gt;trash-put&lt;/code&gt; from the &lt;code&gt;trash-cli&lt;/code&gt; package, which moves files to the desktop trash so they can be restored. Whichever you choose, make it a deliberate habit rather than relying on luck.&lt;/p&gt;
&lt;h2&gt;Functions for the rest&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;mkcd() {
  mkdir -p -- &quot;$1&quot; &amp;amp;&amp;amp; cd -- &quot;$1&quot;
}

extract() {
  case &quot;$1&quot; in
    *.tar.gz|*.tgz) tar xzf &quot;$1&quot; ;;
    *.tar.bz2)      tar xjf &quot;$1&quot; ;;
    *.tar.xz)       tar xJf &quot;$1&quot; ;;
    *.zip)          unzip &quot;$1&quot; ;;
    *.gz)           gunzip &quot;$1&quot; ;;
    *) echo &quot;extract: unknown format: $1&quot; &amp;gt;&amp;amp;2; return 1 ;;
  esac
}

alias ports=&apos;ss -tulpn&apos;

note() {
  local dir=&quot;$HOME/notes&quot;
  mkdir -p &quot;$dir&quot;
  echo &quot;$(date +%H:%M) $*&quot; &amp;gt;&amp;gt; &quot;$dir/$(date +%F).md&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;mkcd&lt;/code&gt; creates a directory and enters it, which no alias can do cleanly. &lt;code&gt;extract&lt;/code&gt; saves you from remembering &lt;code&gt;tar&lt;/code&gt; flags. &lt;code&gt;ports&lt;/code&gt; lists listening TCP and UDP sockets with their processes (run it with &lt;code&gt;sudo&lt;/code&gt; to see processes you do not own). &lt;code&gt;note&lt;/code&gt; appends a timestamped line to a file named after today’s date, so &lt;code&gt;note fix flaky test in CI&lt;/code&gt; is a one-second capture. My notes CLI grew out of exactly this function.&lt;/p&gt;
&lt;h2&gt;Where to put them&lt;/h2&gt;
&lt;p&gt;Bash reads &lt;code&gt;~/.bashrc&lt;/code&gt; for interactive shells. Many distributions already source &lt;code&gt;~/.bash_aliases&lt;/code&gt; from it, which keeps your additions separate. If yours does not, add one line:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-diff&quot;&gt; # ~/.bashrc
 export EDITOR=vim
+[ -f ~/.bash_aliases ] &amp;amp;&amp;amp; . ~/.bash_aliases
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then reload with &lt;code&gt;source ~/.bashrc&lt;/code&gt;. For zsh, the same code goes in &lt;code&gt;~/.zshrc&lt;/code&gt; (or a sourced file); the aliases and the functions above work unchanged.&lt;/p&gt;
&lt;h2&gt;Inspecting what you have&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;alias            # list all aliases
alias gs         # show one
type gs          # gs is aliased to `git status -sb&apos;
type mkcd        # shows the function body
unalias gs       # remove for this session
command rm file  # bypass the alias once (or use \rm)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;type&lt;/code&gt; is the most useful of these because it tells you whether a name is an alias, a function, a builtin, or a file on disk.&lt;/p&gt;
&lt;h2&gt;Keeping them tidy and portable&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Keep one file under version control, such as a dotfiles repository, and symlink it.&lt;/li&gt;
&lt;li&gt;Prefer descriptive function names over two-letter cryptic ones unless you type them constantly.&lt;/li&gt;
&lt;li&gt;Avoid shadowing common commands with different behavior; scripts you run interactively may surprise you.&lt;/li&gt;
&lt;li&gt;Guard anything non-portable, for example &lt;code&gt;command -v trash-put &amp;gt;/dev/null &amp;amp;&amp;amp; alias rm=&apos;trash-put&apos;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Remove what you have not used in months.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you are still building up your command-line basics, &lt;a href=&quot;https://portofolio-xi-nine-80.vercel.app/blog/linux-terminal-essentials/&quot;&gt;Linux terminal essentials&lt;/a&gt; is a good companion.&lt;/p&gt;
&lt;h2&gt;What to do next&lt;/h2&gt;
&lt;p&gt;Run &lt;code&gt;history | awk &apos;{print $2}&apos; | sort | uniq -c | sort -rn | head&lt;/code&gt; to see your most-used commands, then turn the top three into aliases today. Add a function only when an alias stops being enough.&lt;/p&gt;
</content:encoded><category>Tips &amp; Tricks</category><category>bash</category><category>cli</category><category>productivity</category><category>terminal</category><author>perdidev@gmail.com (Perdi)</author></item><item><title>TypeScript Generics With Real Examples</title><link>https://portofolio-xi-nine-80.vercel.app/blog/typescript-generics-in-practice/</link><guid isPermaLink="true">https://portofolio-xi-nine-80.vercel.app/blog/typescript-generics-in-practice/</guid><description>Learn TypeScript generics through small, practical helpers: constraints, typed fetches, Result types, keyof, defaults, and when to skip them.</description><pubDate>Tue, 07 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Generics look intimidating in library typings, but in everyday code they solve one simple problem: keeping type information that would otherwise be thrown away. I will build up from small helpers.&lt;/p&gt;
&lt;h2&gt;The problem: any loses information&lt;/h2&gt;
&lt;p&gt;Say you write a helper that returns the first item of an array.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;function firstAny(items: any[]): any {
  return items[0];
}

const n = firstAny([1, 2, 3]); // any, so n.toFixed() and n.nope() both compile
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The caller gets &lt;code&gt;any&lt;/code&gt; back, so the compiler stops helping. A type parameter fixes that by linking the input type to the output type.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;export function first&amp;lt;T&amp;gt;(items: T[]): T | undefined {
  return items[0];
}

const n = first([1, 2, 3]); // number | undefined
const s = first([&quot;a&quot;, &quot;b&quot;]); // string | undefined
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;T&lt;/code&gt; is inferred from the argument. You rarely need to write &lt;code&gt;first&amp;lt;number&amp;gt;(...)&lt;/code&gt; yourself.&lt;/p&gt;
&lt;h2&gt;Constraints with extends&lt;/h2&gt;
&lt;p&gt;Sometimes the function needs to know something about &lt;code&gt;T&lt;/code&gt;. A constraint says “any type, as long as it has this shape”.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;export function byId&amp;lt;T extends { id: string }&amp;gt;(
  items: T[],
  id: string,
): T | undefined {
  return items.find((item) =&amp;gt; item.id === id);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Callers can pass any array of objects with a string &lt;code&gt;id&lt;/code&gt;, and they get back their own full type, not just &lt;code&gt;{ id: string }&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;A typed fetchJson, and its limits&lt;/h2&gt;
&lt;p&gt;A generic return type is handy for API helpers. In my budgeting app, I use one like this for every endpoint:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;interface Transaction {
  id: string;
  amount: number;
  category: string;
}

export async function fetchJson&amp;lt;T&amp;gt;(url: string): Promise&amp;lt;T&amp;gt; {
  const res = await fetch(url);
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  return (await res.json()) as T;
}

async function load(): Promise&amp;lt;Transaction[]&amp;gt; {
  return fetchJson&amp;lt;Transaction[]&amp;gt;(&quot;/api/transactions&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Be honest about what this does: &lt;code&gt;as T&lt;/code&gt; is a cast. TypeScript trusts you, and nothing is checked at runtime. If the server changes its response, the types will still claim everything is fine. For data crossing a trust boundary, validate it with a schema library such as Zod and infer the type from the schema.&lt;/p&gt;
&lt;h2&gt;Generic types&lt;/h2&gt;
&lt;p&gt;Types can take parameters too. A &lt;code&gt;Result&lt;/code&gt; type makes failures explicit instead of relying on exceptions.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;export type Result&amp;lt;T, E = Error&amp;gt; =
  | { ok: true; value: T }
  | { ok: false; error: E };

export function parseAmount(input: string): Result&amp;lt;number&amp;gt; {
  const n = Number(input);
  return Number.isFinite(n) &amp;amp;&amp;amp; input.trim() !== &quot;&quot;
    ? { ok: true, value: n }
    : { ok: false, error: new Error(`Invalid amount: ${input}`) };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Checking &lt;code&gt;result.ok&lt;/code&gt; narrows the union, so &lt;code&gt;value&lt;/code&gt; and &lt;code&gt;error&lt;/code&gt; are only available where they exist. The &lt;code&gt;= Error&lt;/code&gt; part is a default type parameter: callers can write &lt;code&gt;Result&amp;lt;number&amp;gt;&lt;/code&gt; and only spell out the second argument when they need a custom error type.&lt;/p&gt;
&lt;h2&gt;keyof for safe property access&lt;/h2&gt;
&lt;p&gt;To read a property by name, constrain the key to the keys of the object.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;export function pluck&amp;lt;T, K extends keyof T&amp;gt;(items: T[], key: K): T[K][] {
  return items.map((item) =&amp;gt; item[key]);
}

// amounts is number[]; pluck(transactions, &quot;amout&quot;) is a compile error
const amounts = pluck([{ id: &quot;1&quot;, amount: 12 }], &quot;amount&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The return type &lt;code&gt;T[K][]&lt;/code&gt; follows the chosen key, so &lt;code&gt;&quot;amount&quot;&lt;/code&gt; gives &lt;code&gt;number[]&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;When not to use generics&lt;/h2&gt;
&lt;p&gt;If a type parameter appears only once in the signature, it is not relating anything to anything. It is probably unnecessary.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;function printLength&amp;lt;T extends { length: number }&amp;gt;(value: T): void {
function printLength(value: { length: number }): void {
  console.log(value.length);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Both versions behave the same, but the second is easier to read. Reach for a generic when the output depends on the input, or when two parameters must share a type.&lt;/p&gt;
&lt;h2&gt;What to do next&lt;/h2&gt;
&lt;p&gt;Look through your codebase for helpers typed with &lt;code&gt;any&lt;/code&gt; or &lt;code&gt;unknown&lt;/code&gt; plus a cast, and check whether a type parameter would remove the cast. If you want a refresher on handling the async side of &lt;code&gt;fetchJson&lt;/code&gt;, see &lt;a href=&quot;https://portofolio-xi-nine-80.vercel.app/blog/async-await-in-javascript/&quot;&gt;async/await in JavaScript without the confusion&lt;/a&gt;.&lt;/p&gt;
</content:encoded><category>Programming</category><category>typescript</category><category>generics</category><category>javascript</category><category>api-design</category><author>perdidev@gmail.com (Perdi)</author></item><item><title>systemd Basics: Services, Units, and Timers</title><link>https://portofolio-xi-nine-80.vercel.app/blog/systemd-basics/</link><guid isPermaLink="true">https://portofolio-xi-nine-80.vercel.app/blog/systemd-basics/</guid><description>A practical introduction to systemd: manage services with systemctl, write a unit file, read journal logs, and replace cron with a timer.</description><pubDate>Tue, 19 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;On most modern Linux distributions, systemd is the first process the kernel starts, PID 1. It brings up the system, supervises services, collects their logs, and schedules recurring jobs. You do not need to master all of it, but a working knowledge pays off the first time you deploy something that must stay running.&lt;/p&gt;
&lt;h2&gt;Units in one minute&lt;/h2&gt;
&lt;p&gt;systemd manages resources described by unit files. The type is the file extension:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;.service&lt;/code&gt; runs a daemon or a one-shot command.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.timer&lt;/code&gt; triggers another unit on a schedule.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.socket&lt;/code&gt;, &lt;code&gt;.mount&lt;/code&gt;, and &lt;code&gt;.target&lt;/code&gt; cover sockets, filesystems, and groups of units.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Your own system-wide units belong in &lt;code&gt;/etc/systemd/system/&lt;/code&gt;, while packages install theirs under &lt;code&gt;/usr/lib/systemd/system/&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;systemctl essentials&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;systemctl status nginx          # state, recent logs, main PID
sudo systemctl start nginx
sudo systemctl stop nginx
sudo systemctl restart nginx
sudo systemctl enable nginx     # start at boot
sudo systemctl enable --now nginx   # enable and start in one go
sudo systemctl disable nginx
sudo systemctl daemon-reload    # re-read unit files after editing
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Remember the difference: &lt;code&gt;start&lt;/code&gt; affects now, &lt;code&gt;enable&lt;/code&gt; affects boot. After editing any unit file, run &lt;code&gt;daemon-reload&lt;/code&gt; or systemd keeps using the old version.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;status&lt;/code&gt; output is worth reading closely. It shows whether the unit is active, when it started, its main PID, and the last few log lines, which is often enough to diagnose a failure without opening anything else.&lt;/p&gt;
&lt;p&gt;For services that belong to your user rather than the system, add &lt;code&gt;--user&lt;/code&gt;. Units go in &lt;code&gt;~/.config/systemd/user/&lt;/code&gt;, and no &lt;code&gt;sudo&lt;/code&gt; is needed:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;systemctl --user status notes-backup.service
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Writing a small service&lt;/h2&gt;
&lt;p&gt;Say I have a script that backs up my notes directory. It runs, finishes, and exits, so a one-shot service is enough. The &lt;code&gt;[Unit]&lt;/code&gt; section describes the service and its ordering, and &lt;code&gt;[Service]&lt;/code&gt; says what to execute and as whom:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ini&quot;&gt;[Unit]
Description=Back up notes to the archive disk
After=network-online.target

[Service]
Type=oneshot
User=perdi
ExecStart=/usr/local/bin/notes-backup.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For a long-running process, you will want it to come back after a crash. Here is the edit:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-diff&quot;&gt; [Service]
 ExecStart=/usr/local/bin/notes-server
+Restart=on-failure
+RestartSec=5
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;on-failure&lt;/code&gt; restarts the process only after a non-zero exit or a crash, not when you stop it deliberately, and &lt;code&gt;RestartSec&lt;/code&gt; avoids a tight restart loop. Save, run &lt;code&gt;sudo systemctl daemon-reload&lt;/code&gt;, then restart the service. Make sure the script is executable; the permissions article on &lt;a href=&quot;https://portofolio-xi-nine-80.vercel.app/blog/linux-file-permissions/&quot;&gt;Linux file permissions&lt;/a&gt; explains why that matters.&lt;/p&gt;
&lt;h2&gt;Reading logs with journalctl&lt;/h2&gt;
&lt;p&gt;systemd captures stdout and stderr from services into the journal, so you rarely need a custom log file or a log rotation setup. Every line is stored with a timestamp and the unit that produced it, which makes filtering simple.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;journalctl -u notes-backup.service            # everything for the unit
journalctl -u notes-backup.service -f         # follow live
journalctl -u notes-backup.service --since &quot;1 hour ago&quot;
journalctl -u nginx --since today -p err      # only errors
journalctl -b                                 # current boot
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Replacing cron with a timer&lt;/h2&gt;
&lt;p&gt;A timer is a second unit that activates the service with the same base name. Create it next to the service:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ini&quot;&gt;[Unit]
Description=Run notes backup daily

[Timer]
OnCalendar=daily
Persistent=true

[Install]
WantedBy=timers.target
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Persistent=true&lt;/code&gt; is the feature I miss most in cron: if the machine was off at the scheduled time, the job runs at the next boot. Enable the timer, not the service:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo systemctl daemon-reload
sudo systemctl enable --now notes-backup.timer
systemctl list-timers --all             # next and last run times
systemd-analyze calendar &quot;Mon *-*-* 03:00&quot;   # validate a schedule
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Compared with cron, you gain logging through the journal, dependency ordering, and a clear status view. The cost is two files instead of one line. The pairing works by name: &lt;code&gt;notes-backup.timer&lt;/code&gt; starts &lt;code&gt;notes-backup.service&lt;/code&gt; automatically, so you never enable the service itself.&lt;/p&gt;
&lt;h2&gt;What to do next&lt;/h2&gt;
&lt;p&gt;Pick one cron job or &lt;code&gt;nohup&lt;/code&gt; command you currently rely on and convert it to a unit. Start with &lt;code&gt;--user&lt;/code&gt; units so mistakes stay in your home directory, and check the result with &lt;code&gt;systemctl status&lt;/code&gt; and &lt;code&gt;journalctl -u&lt;/code&gt;. Once that works, add &lt;code&gt;Restart=&lt;/code&gt; and resource limits as needed.&lt;/p&gt;
</content:encoded><category>Linux</category><category>linux</category><category>systemd</category><category>services</category><category>bash</category><author>perdidev@gmail.com (Perdi)</author></item><item><title>Writing Good Commit Messages</title><link>https://portofolio-xi-nine-80.vercel.app/blog/writing-good-commit-messages/</link><guid isPermaLink="true">https://portofolio-xi-nine-80.vercel.app/blog/writing-good-commit-messages/</guid><description>How to write commit messages that explain why a change happened, from subject line to footers, and how to keep history clean with amend and autosquash.</description><pubDate>Tue, 08 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Why history is documentation&lt;/h2&gt;
&lt;p&gt;A commit message is the one piece of documentation that is written at the exact moment a change is made. Six months from now, someone (very often me) will run &lt;code&gt;git blame&lt;/code&gt; on a strange line and wonder why it exists. The diff shows what changed. Only the message can say why.&lt;/p&gt;
&lt;p&gt;Good messages also make reviews faster and &lt;code&gt;git bisect&lt;/code&gt; output readable. I treat the message as part of the change, not as paperwork after it.&lt;/p&gt;
&lt;h2&gt;Anatomy of a commit message&lt;/h2&gt;
&lt;p&gt;A message has up to three parts, separated by blank lines:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Subject:&lt;/strong&gt; imperative mood, around 50 characters, no trailing period.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Body:&lt;/strong&gt; explains why the change was needed, wrapped at about 72 characters.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Footers:&lt;/strong&gt; references and credits, one per line.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;The subject line&lt;/h3&gt;
&lt;p&gt;I write it so it completes the sentence “If applied, this commit will …”. “Add retry to the upload client” works. “Added retry” and “Adding retry” do not. A short subject also survives &lt;code&gt;git log --oneline&lt;/code&gt; without being truncated.&lt;/p&gt;
&lt;h3&gt;The body and footers&lt;/h3&gt;
&lt;p&gt;The body is for context the diff cannot carry: the symptom, the constraint, the alternative you rejected. Skip the line-by-line retelling of the code. Footers such as &lt;code&gt;Fixes #123&lt;/code&gt; let the tracker close the issue, and &lt;code&gt;Co-authored-by&lt;/code&gt; gives credit to pair programming partners.&lt;/p&gt;
&lt;p&gt;Passing &lt;code&gt;-m&lt;/code&gt; twice gives you a subject and a body without opening an editor:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git commit -m &quot;Fix off-by-one in pagination offset&quot; -m &quot;The offset was computed from the page number instead of the
zero-based page index, so the first page skipped ten items.
Use page - 1 and add a regression test.

Fixes #123
Co-authored-by: Ana Silva &amp;lt;ana@example.com&amp;gt;&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Bad messages and better ones&lt;/h2&gt;
&lt;p&gt;Most weak messages fail the same way: they say nothing a reader could search for.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;fix stuff&lt;/td&gt;
&lt;td&gt;Fix off-by-one in pagination offset&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;updates&lt;/td&gt;
&lt;td&gt;Bump sharp to 0.33 to fix ARM build failures&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WIP&lt;/td&gt;
&lt;td&gt;Extract token parsing into its own module&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;address review&lt;/td&gt;
&lt;td&gt;Reject empty slugs in the post loader&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The “after” versions name the thing that changed and hint at the reason. You can check how your own history reads at any time:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git log --oneline -5
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Keep commits atomic&lt;/h2&gt;
&lt;p&gt;One commit should hold one logical change. If my working tree contains a refactor and a bug fix, I stage them separately with &lt;code&gt;git add -p&lt;/code&gt;, which walks through each hunk and asks whether to include it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git add -p
git commit
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Small commits are easier to review and revert. If you cannot summarize a commit without the word “and”, it is probably two commits. This pairs well with the habits in &lt;a href=&quot;https://portofolio-xi-nine-80.vercel.app/blog/effective-code-reviews/&quot;&gt;How to Run Effective Code Reviews&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Fixing history before you share it&lt;/h2&gt;
&lt;p&gt;Typo in the last message, or a forgotten file? Amend it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git commit --amend
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For a mistake in an older commit on my own branch, I create a fixup commit and let Git move it into place:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git commit --fixup a1b2c3d
git rebase -i --autosquash main
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Only rewrite commits that nobody else has based work on. Once a branch is shared, add a new commit instead.&lt;/p&gt;
&lt;h2&gt;A note on Conventional Commits&lt;/h2&gt;
&lt;p&gt;Prefixes like &lt;code&gt;feat:&lt;/code&gt; and &lt;code&gt;fix:&lt;/code&gt; make changelogs and version bumps automatable, and they force a small amount of classification. The trade-off is that the prefix eats into your 50 characters, and people often pick a type just to satisfy a linter while the message itself stays vague. I find them worthwhile when tooling consumes them and optional otherwise.&lt;/p&gt;
&lt;h2&gt;A convention for your team&lt;/h2&gt;
&lt;p&gt;Agreeing on a few rules matters more than which rules you pick:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Imperative subject, about 50 characters, no period.&lt;/li&gt;
&lt;li&gt;A body whenever the reason is not obvious from the diff.&lt;/li&gt;
&lt;li&gt;A reference to the issue or ticket in a footer.&lt;/li&gt;
&lt;li&gt;Squash fixup commits before merging.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Write this down in your contributing guide, then try it for two weeks. The next time you chase a bug with &lt;a href=&quot;https://portofolio-xi-nine-80.vercel.app/blog/debugging-habits/&quot;&gt;good debugging habits&lt;/a&gt;, notice how often the answer is sitting in &lt;code&gt;git log&lt;/code&gt;.&lt;/p&gt;
</content:encoded><category>Software Engineering</category><category>git</category><category>workflow</category><category>collaboration</category><author>perdidev@gmail.com (Perdi)</author></item><item><title>Linux File Permissions: chmod, chown, and umask</title><link>https://portofolio-xi-nine-80.vercel.app/blog/linux-file-permissions/</link><guid isPermaLink="true">https://portofolio-xi-nine-80.vercel.app/blog/linux-file-permissions/</guid><description>Learn to read ls -l output and use chmod, chown, and umask correctly, plus setuid, setgid, and sticky bits and the mistakes to avoid.</description><pubDate>Tue, 27 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Permission errors are one of the first walls newcomers hit on Linux, and “fixing” them with brute force is a habit that causes real security problems. The model is small enough to learn in one sitting, so let’s do that.&lt;/p&gt;
&lt;h2&gt;Reading ls -l output&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ls -l deploy.sh
# -rwxr-xr-- 1 perdi devs 1204 Mar  3 10:12 deploy.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The first column is ten characters. The first is the type (&lt;code&gt;-&lt;/code&gt; file, &lt;code&gt;d&lt;/code&gt; directory, &lt;code&gt;l&lt;/code&gt; symlink). The remaining nine are three triplets: owner (user), group, and others. Each triplet shows read (&lt;code&gt;r&lt;/code&gt;), write (&lt;code&gt;w&lt;/code&gt;), and execute (&lt;code&gt;x&lt;/code&gt;), or &lt;code&gt;-&lt;/code&gt; when missing. After the link count come the owning user (&lt;code&gt;perdi&lt;/code&gt;) and owning group (&lt;code&gt;devs&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;The kernel checks in order: if you are the owner, the owner triplet applies; otherwise, if you are in the group, the group triplet applies; otherwise the others triplet does. It never combines them.&lt;/p&gt;
&lt;h2&gt;Numeric and symbolic chmod&lt;/h2&gt;
&lt;p&gt;Each permission has a value: read is 4, write is 2, execute is 1. Add them up per triplet and you get an octal digit. For &lt;code&gt;754&lt;/code&gt;, the owner gets rwx (7), the group gets r-x (5), and others get r-- (4).&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://portofolio-xi-nine-80.vercel.app/_astro/permission-bits.PM3JAbQz_157xkR.webp&quot; alt=&quot;Diagram showing chmod 754 split into owner, group and others, each triplet summed from read 4, write 2 and execute 1&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Both styles do the same job:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;chmod 754 deploy.sh          # numeric: set everything at once
chmod u=rwx,g=rx,o=r deploy.sh   # symbolic: same result
chmod +x script.sh           # add execute for everyone
chmod g-w,o-rwx secrets.env  # tweak only what you mean to
chmod -R u+rwX,go-w project/ # capital X: execute only on directories
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I prefer symbolic mode for small adjustments because it does not clobber bits I did not mention.&lt;/p&gt;
&lt;h2&gt;Directories behave differently&lt;/h2&gt;
&lt;p&gt;On a directory, &lt;code&gt;r&lt;/code&gt; lets you list names, &lt;code&gt;w&lt;/code&gt; lets you create or delete entries inside, and &lt;code&gt;x&lt;/code&gt; lets you traverse it, meaning access items by path. Without &lt;code&gt;x&lt;/code&gt; on a directory you cannot &lt;code&gt;cd&lt;/code&gt; into it or open files within, even if the files themselves are readable. This is why web server errors often trace back to a parent directory missing &lt;code&gt;x&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Changing ownership&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo chown perdi file.txt              # change owner
sudo chown perdi:devs file.txt         # owner and group
sudo chown -R www-data:www-data /srv/site
chgrp devs shared/                     # change group only
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Only root can change a file’s owner. A regular user can change the group to one they belong to.&lt;/p&gt;
&lt;h2&gt;What umask does&lt;/h2&gt;
&lt;p&gt;New files do not start at 777. The shell’s &lt;code&gt;umask&lt;/code&gt; removes bits from the defaults: 666 for files, 777 for directories. With the common &lt;code&gt;022&lt;/code&gt;, write is removed for group and others, giving 644 files and 755 directories.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;umask          # 0022
umask 077      # private by default: 600 files, 700 directories
touch a &amp;amp;&amp;amp; mkdir b &amp;amp;&amp;amp; ls -ld a b
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Set it in &lt;code&gt;~/.bashrc&lt;/code&gt; if you want a stricter default. A umask of &lt;code&gt;077&lt;/code&gt; suits a personal machine holding keys and tokens.&lt;/p&gt;
&lt;h2&gt;setuid, setgid, and the sticky bit&lt;/h2&gt;
&lt;p&gt;These are special bits shown in a fourth, leading octal digit.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;setuid (4): an executable runs as its owner. &lt;code&gt;passwd&lt;/code&gt; uses this.&lt;/li&gt;
&lt;li&gt;setgid (2): on a directory, new files inherit the directory’s group, which is useful for shared project folders.&lt;/li&gt;
&lt;li&gt;sticky (1): in a directory like &lt;code&gt;/tmp&lt;/code&gt;, only a file’s owner can delete it.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;chmod 2775 shared/    # setgid + rwxrwxr-x
chmod +t /srv/drop    # sticky bit
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Be careful with setuid on anything you write yourself.&lt;/p&gt;
&lt;h2&gt;Common mistakes&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Running &lt;code&gt;chmod 777&lt;/code&gt; to make an error disappear. It lets every user modify the file, and it hides the real cause. Find out which user needs access and grant exactly that.&lt;/li&gt;
&lt;li&gt;Using &lt;code&gt;chmod -R 755&lt;/code&gt; on a tree, which marks every data file executable.&lt;/li&gt;
&lt;li&gt;Making SSH keys group- or world-readable; &lt;code&gt;ssh&lt;/code&gt; refuses them. Use &lt;code&gt;chmod 600 ~/.ssh/id_ed25519&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Forgetting that &lt;code&gt;sudo&lt;/code&gt; creates root-owned files in your project.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;What to do next&lt;/h2&gt;
&lt;p&gt;Run &lt;code&gt;ls -l&lt;/code&gt; in your home directory and explain every column to yourself. Then audit one real project: check what owns your deployment directory and whether any file is more open than it needs to be. If you are still getting comfortable with the shell, start with &lt;a href=&quot;https://portofolio-xi-nine-80.vercel.app/blog/linux-terminal-essentials/&quot;&gt;Linux terminal essentials&lt;/a&gt;.&lt;/p&gt;
</content:encoded><category>Linux</category><category>linux</category><category>permissions</category><category>security</category><category>cli</category><author>perdidev@gmail.com (Perdi)</author></item><item><title>Async/Await in JavaScript Without the Confusion</title><link>https://portofolio-xi-nine-80.vercel.app/blog/async-await-in-javascript/</link><guid isPermaLink="true">https://portofolio-xi-nine-80.vercel.app/blog/async-await-in-javascript/</guid><description>A practical guide to async/await: what await really does, error handling, parallel work with Promise.all, timeouts, and the mistakes I see most often.</description><pubDate>Tue, 22 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Most async bugs I have run into come from a fuzzy mental model, not from the syntax. Once the model is clear, &lt;code&gt;async/await&lt;/code&gt; is pleasantly boring. Here is the version I wish I had been given earlier.&lt;/p&gt;
&lt;h2&gt;Promises are values for “later”&lt;/h2&gt;
&lt;p&gt;A promise is an ordinary object that stands in for a result that is not ready yet. It is pending, then it either fulfils with a value or rejects with an error. It never changes state again after that.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const response = fetch(&quot;https://example.com/api/users&quot;);
console.log(response); // Promise { &amp;lt;pending&amp;gt; }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;async/await&lt;/code&gt; is syntax on top of promises. An &lt;code&gt;async&lt;/code&gt; function always returns a promise, and &lt;code&gt;await&lt;/code&gt; unwraps one.&lt;/p&gt;
&lt;h2&gt;What await really does&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;await&lt;/code&gt; pauses the async function, not the thread. JavaScript keeps running other code while the awaited promise is pending, and resumes your function afterwards.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;async function loadUser(id) {
  console.log(&quot;start&quot;);
  const res = await fetch(`https://example.com/api/users/${id}`);
  console.log(&quot;got response&quot;);
  return res.json();
}

loadUser(1);
console.log(&quot;runs second&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The output order is &lt;code&gt;start&lt;/code&gt;, &lt;code&gt;runs second&lt;/code&gt;, &lt;code&gt;got response&lt;/code&gt;. The code after &lt;code&gt;loadUser(1)&lt;/code&gt; does not wait for it.&lt;/p&gt;
&lt;h2&gt;Error handling&lt;/h2&gt;
&lt;p&gt;A rejected promise turns into a thrown exception at the &lt;code&gt;await&lt;/code&gt;, so &lt;code&gt;try/catch/finally&lt;/code&gt; works as you would expect.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;async function save(form) {
  setLoading(true);
  try {
    const res = await fetch(&quot;/api/save&quot;, { method: &quot;POST&quot;, body: form });
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
  } catch (err) {
    showError(err);
  } finally {
    setLoading(false);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note the &lt;code&gt;res.ok&lt;/code&gt; check. &lt;code&gt;fetch&lt;/code&gt; only rejects on network failures, so a 404 or 500 arrives as a normal response.&lt;/p&gt;
&lt;h2&gt;Sequential versus parallel&lt;/h2&gt;
&lt;p&gt;The classic mistake is awaiting independent work one item at a time. Each call waits for the previous one, so total time is the sum instead of the maximum.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-diff&quot;&gt;-const user = await getUser(id);
-const orders = await getOrders(id);
-const prefs = await getPrefs(id);
+const [user, orders, prefs] = await Promise.all([
+  getUser(id),
+  getOrders(id),
+  getPrefs(id),
+]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The same applies to loops. &lt;code&gt;await&lt;/code&gt; inside a &lt;code&gt;for...of&lt;/code&gt; is right when each step depends on the last. For independent items, start everything first:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const users = await Promise.all(ids.map((id) =&amp;gt; getUser(id)));
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Partial failure with allSettled&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;Promise.all&lt;/code&gt; rejects as soon as one promise rejects. When you want every result, successful or not, use &lt;code&gt;Promise.allSettled&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const results = await Promise.allSettled(ids.map((id) =&amp;gt; getUser(id)));
const users = results
  .filter((r) =&amp;gt; r.status === &quot;fulfilled&quot;)
  .map((r) =&amp;gt; r.value);
const failures = results.filter((r) =&amp;gt; r.status === &quot;rejected&quot;).length;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Floating promises&lt;/h2&gt;
&lt;p&gt;Forgetting &lt;code&gt;await&lt;/code&gt; is easy and quiet. The function runs, but nothing waits for it and nothing catches its errors.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;save(form); // floating: a rejection becomes an unhandled rejection
await save(form); // fine
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In Node, an unhandled rejection crashes the process by default. In TypeScript projects I enable the &lt;code&gt;@typescript-eslint/no-floating-promises&lt;/code&gt; lint rule so the mistake gets flagged in the editor.&lt;/p&gt;
&lt;h2&gt;Timeouts with AbortSignal&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;fetch&lt;/code&gt; has no timeout by default. &lt;code&gt;AbortSignal.timeout()&lt;/code&gt; gives you one without any manual timer cleanup.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;try {
  const res = await fetch(url, { signal: AbortSignal.timeout(5000) });
  console.log(await res.json());
} catch (err) {
  if (err.name === &quot;TimeoutError&quot;) {
    console.error(&quot;Request took longer than 5 seconds&quot;);
  } else {
    throw err;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;A typed example&lt;/h2&gt;
&lt;p&gt;In TypeScript, an async function returns &lt;code&gt;Promise&amp;lt;T&amp;gt;&lt;/code&gt;, and &lt;code&gt;await&lt;/code&gt; gives you the &lt;code&gt;T&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;interface User {
  id: string;
  name: string;
}

export async function fetchUser(id: string): Promise&amp;lt;User&amp;gt; {
  const res = await fetch(`https://example.com/api/users/${id}`, {
    signal: AbortSignal.timeout(5000),
  });
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  return (await res.json()) as User;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That &lt;code&gt;as User&lt;/code&gt; is only a cast, not a check. I cover how to make this safer in &lt;a href=&quot;https://portofolio-xi-nine-80.vercel.app/blog/typescript-generics-in-practice/&quot;&gt;TypeScript generics with real examples&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;What to do next&lt;/h2&gt;
&lt;p&gt;Search your codebase for &lt;code&gt;await&lt;/code&gt; inside loops and ask whether the iterations are really dependent. Then add a timeout to every network call. When something still behaves strangely, my notes on &lt;a href=&quot;https://portofolio-xi-nine-80.vercel.app/blog/debugging-habits/&quot;&gt;habits for debugging faster&lt;/a&gt; cover how to narrow it down.&lt;/p&gt;
</content:encoded><category>Programming</category><category>javascript</category><category>async</category><category>promises</category><category>typescript</category><author>perdidev@gmail.com (Perdi)</author></item><item><title>Linux Terminal Essentials Every Developer Needs</title><link>https://portofolio-xi-nine-80.vercel.app/blog/linux-terminal-essentials/</link><guid isPermaLink="true">https://portofolio-xi-nine-80.vercel.app/blog/linux-terminal-essentials/</guid><description>The handful of terminal commands and shortcuts I use every day for navigating, searching, piping output, and managing jobs on Linux.</description><pubDate>Wed, 12 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;You can do a lot of development work without ever leaving the terminal, but only if a small core of commands is second nature. This is the set I reach for daily. None of it is exotic, and all of it works in a stock bash installation, so you can rely on it on a fresh server, in a container, or on a colleague’s laptop.&lt;/p&gt;
&lt;h2&gt;Navigating the filesystem&lt;/h2&gt;
&lt;p&gt;Start with knowing where you are and what is around you.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pwd          # print the current directory
ls -la       # long listing, including hidden files
cd -         # jump back to the previous directory
cd           # with no argument, go home
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;cd -&lt;/code&gt; is the one people tend to miss. It toggles between two directories, which is handy when you are bouncing between a source tree and a log folder.&lt;/p&gt;
&lt;p&gt;When you need more than two places, use a directory stack:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pushd ~/projects/api    # go there, remember where you were
pushd /var/log          # stack another one
dirs -v                 # show the stack with indexes
popd                    # return to the previous entry
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Finding things&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;find&lt;/code&gt; locates files by name, type, size, or age. &lt;code&gt;grep&lt;/code&gt; searches inside them.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;find . -name &quot;*.ts&quot; -not -path &quot;./node_modules/*&quot;
find . -type f -mtime -1          # files changed in the last day
grep -rn &quot;TODO&quot; src/              # recursive, with line numbers
grep -rni --include=&quot;*.md&quot; &quot;systemd&quot; .
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you search code often, install ripgrep (&lt;code&gt;rg&lt;/code&gt;). It respects &lt;code&gt;.gitignore&lt;/code&gt;, skips binary files, and is much faster on large repositories. The equivalent of the first grep above is simply &lt;code&gt;rg -n TODO src/&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Pipes and redirection&lt;/h2&gt;
&lt;p&gt;Every program has standard output (1) and standard error (2). Pipes and redirects let you connect them.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ps aux | grep node | wc -l        # chain commands with |
ls -la &amp;gt; listing.txt              # overwrite a file
echo &quot;done&quot; &amp;gt;&amp;gt; build.log          # append to a file
make &amp;gt; build.log 2&amp;gt;&amp;amp;1             # send both streams to one file
make 2&amp;gt;&amp;amp;1 | tee build.log         # watch the output and save it
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The order in &lt;code&gt;2&amp;gt;&amp;amp;1&lt;/code&gt; matters: it means “send stderr to wherever stdout points right now”, so it must come after the &lt;code&gt;&amp;gt;&lt;/code&gt; redirect. &lt;code&gt;tee&lt;/code&gt; is the right tool when you want to see output live and keep a copy.&lt;/p&gt;
&lt;h2&gt;Viewing files&lt;/h2&gt;
&lt;p&gt;Avoid dumping large files with &lt;code&gt;cat&lt;/code&gt;. Use a pager or look at the edges.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;less +F app.log      # follow mode; Ctrl-C to stop following, q to quit
head -n 20 data.csv  # first 20 lines
tail -n 50 app.log   # last 50 lines
tail -f app.log      # stream new lines as they arrive
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Inside &lt;code&gt;less&lt;/code&gt;, &lt;code&gt;/pattern&lt;/code&gt; searches forward, &lt;code&gt;n&lt;/code&gt; jumps to the next match, and &lt;code&gt;G&lt;/code&gt; goes to the end.&lt;/p&gt;
&lt;h2&gt;Job control&lt;/h2&gt;
&lt;p&gt;A long-running command does not have to block your shell.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Ctrl-C&lt;/code&gt; stops the foreground process.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Ctrl-Z&lt;/code&gt; suspends it and returns your prompt.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;bg&lt;/code&gt; resumes the suspended job in the background.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fg&lt;/code&gt; brings it back to the foreground.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;jobs&lt;/code&gt; lists what the current shell is managing.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm run build         # takes a while... press Ctrl-Z
bg                    # let it continue in the background
jobs                  # [1]+ Running  npm run build &amp;amp;
fg %1                 # bring job 1 back
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For work that must survive closing the terminal, look at &lt;code&gt;tmux&lt;/code&gt; or a proper service; I cover the latter in &lt;a href=&quot;https://portofolio-xi-nine-80.vercel.app/blog/systemd-basics/&quot;&gt;systemd basics&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Searching your history&lt;/h2&gt;
&lt;p&gt;Press &lt;code&gt;Ctrl-R&lt;/code&gt; and start typing. Bash searches backwards through history for a matching command; press &lt;code&gt;Ctrl-R&lt;/code&gt; again to cycle to older matches, &lt;code&gt;Enter&lt;/code&gt; to run, or the right arrow to edit first. Combined with &lt;code&gt;history | grep docker&lt;/code&gt;, you rarely need to retype anything long.&lt;/p&gt;
&lt;h2&gt;Habits worth building&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Tab-complete everything. It prevents typos and teaches you what exists.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;man&lt;/code&gt; and &lt;code&gt;--help&lt;/code&gt; before searching the web; &lt;code&gt;tldr&lt;/code&gt; pages are a nice shortcut for examples.&lt;/li&gt;
&lt;li&gt;Quote paths and patterns that contain spaces or wildcards.&lt;/li&gt;
&lt;li&gt;Preview destructive commands first, for example run the &lt;code&gt;find&lt;/code&gt; command on its own before adding &lt;code&gt;-delete&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Turn repeated command lines into aliases once they annoy you. I wrote about that in &lt;a href=&quot;https://portofolio-xi-nine-80.vercel.app/blog/shell-aliases-and-functions/&quot;&gt;shell aliases and functions&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;What to do next&lt;/h2&gt;
&lt;p&gt;Pick two items from this list that you do not use yet, such as &lt;code&gt;cd -&lt;/code&gt; and &lt;code&gt;Ctrl-R&lt;/code&gt;, and force yourself to use them for a week. Once they are automatic, add the next pair. Small, deliberate repetition beats reading another cheat sheet.&lt;/p&gt;
</content:encoded><category>Linux</category><category>linux</category><category>terminal</category><category>bash</category><category>cli</category><author>perdidev@gmail.com (Perdi)</author></item></channel></rss>