Skip to content

Comments

Fix issue where command history added to shell was incompatible with fish format, corrupting history#142

Open
mooz wants to merge 1 commit intoBuilderIO:mainfrom
mooz:fix-shell-history
Open

Fix issue where command history added to shell was incompatible with fish format, corrupting history#142
mooz wants to merge 1 commit intoBuilderIO:mainfrom
mooz:fix-shell-history

Conversation

@mooz
Copy link

@mooz mooz commented Mar 23, 2025

Fix fish shell history format handling

Problem

The current implementation corrupts the fish shell history file by appending commands in an incorrect format. The fish shell history file uses a YAML-like format rather than simple newline-separated entries.

Solution

Implemented shell-specific handlers to properly read and write to each shell's history file according to its specific format.

Changes

  • Created a ShellHistoryHandler interface
  • Implemented a SimpleHistoryHandler class for basic shells (bash, sh, ksh, tcsh)
  • Implemented a ZshHistoryHandler class for zsh (handling timestamp format)
  • Implemented a FishHistoryHandler class for fish (handling YAML-like format)
  • Updated main functions to use the appropriate handler based on the detected shell

Testing

  • Verified history files are correctly updated after command execution in bash, zsh, and fish shells
  • Specifically confirmed the fish shell history file is no longer corrupted

@steve8708
Copy link
Contributor

thanks @mooz - looks like build not passing though

Copy link

@darshjme-codes darshjme-codes left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent architecture! 🏛️ Proper abstraction for shell-specific formats.

Code Quality

The strategy pattern implementation is textbook-perfect:

  • ShellHistoryHandler interface provides clean abstraction
  • ✅ Separate handlers for Simple, Zsh, and Fish formats
  • ✅ Inheritance: ZshHistoryHandler extends SimpleHistoryHandler (DRY)
  • ✅ Comprehensive shell coverage (bash, zsh, fish, ksh, tcsh)

Fish History Implementation

The Fish handler correctly uses the YAML-like format:

- cmd: command
  when: timestamp

Suggestion: Fish's timestamp is in Unix seconds. The current implementation is correct (Math.floor(Date.now() / 1000)), but consider adding a comment:

const timestamp = Math.floor(Date.now() / 1000); // Unix timestamp in seconds (fish format)

Edge Case: Fish history entries can also have a paths: field for command-specific context. While not required, consider documenting this limitation:

// Note: We only write cmd and when fields. Fish supports additional fields (paths, command_duration)
// but they're optional and not critical for basic history functionality.

Zsh Timestamp Regex

The regex for zsh extended history is solid:

const match = line.match(/^(?::\s*\d+:\d+;)?(.*)$/);

This handles both formats:

  • Standard: command
  • Extended: : timestamp:duration;command

Suggestion: Add a test comment explaining the format:

// Zsh extended history format: ": timestamp:duration;command"
// Standard format: "command"
const match = line.match(/^(?::\s*\d+:\d+;)?(.*)$/);

Potential Issue: Async File Append Race Condition

The appendCommand methods use async fs.appendFile without waiting:

appendCommand(historyFile: string, command: string): void {
  fs.appendFile(historyFile, `${command}\n`, (err) => {
    if (err) {
      // Ignore any errors
    }
  });
}

If appendToShellHistory() is called multiple times rapidly, writes may interleave incorrectly. Consider:

Option 1: Synchronous writes (simplest, acceptable for small writes):

appendCommand(historyFile: string, command: string): void {
  try {
    fs.appendFileSync(historyFile, `${command}\n`);
  } catch (err) {
    // Ignore errors
  }
}

Option 2: Queue with async/await (if async is important):

private writeQueue = Promise.resolve();

appendCommand(historyFile: string, command: string): void {
  this.writeQueue = this.writeQueue.then(() => 
    fs.promises.appendFile(historyFile, `${command}\n`).catch(() => {})
  );
}

Given history writes are rare (per command execution), synchronous writes are likely fine and simpler.

Error Handling: Silent Failures

All methods silently ignore errors. While this is reasonable for history (don't break the CLI if history fails), consider:

  1. Optional logging for debugging:
catch (err) {
  if (process.env.DEBUG_SHELL_HISTORY) {
    console.error('History write failed:', err);
  }
}
  1. Return success boolean for testing:
appendCommand(historyFile: string, command: string): boolean {
  try {
    fs.appendFileSync(historyFile, `${command}\n`);
    return true;
  } catch (err) {
    return false;
  }
}

Testing Recommendations

  1. Fish format validation:
test('FishHistoryHandler writes correct YAML format', () => {
  const handler = new FishHistoryHandler('/tmp/test_fish_history');
  handler.appendCommand('/tmp/test_fish_history', 'echo test');
  
  const content = fs.readFileSync('/tmp/test_fish_history', 'utf8');
  expect(content).toMatch(/^- cmd: echo test\n  when: \d+\n$/);
});
  1. Zsh extended format parsing:
test('ZshHistoryHandler parses extended format', () => {
  const testData = ': 1234567890:0;git status\n: 1234567891:5;npm test';
  fs.writeFileSync('/tmp/test_zsh', testData);
  
  const handler = new ZshHistoryHandler('/tmp/test_zsh');
  expect(handler.getLastCommand('/tmp/test_zsh')).toBe('npm test');
});
  1. Duplicate prevention:
test('appendToShellHistory prevents duplicates', () => {
  // Write same command twice, verify only one entry added
});
  1. Race condition test:
test('handles rapid successive appends', async () => {
  for (let i = 0; i < 100; i++) {
    appendToShellHistory(`command_${i}`);
  }
  // Verify all 100 commands are in history with no corruption
});

Documentation

Add comments to the file explaining:

  • The purpose of each handler (which shells they support)
  • Why silent error handling is acceptable for history
  • Examples of each shell's history format

Example:

/**
 * Handles shell history file updates across different shells.
 * Each shell uses a different format:
 * - bash/sh/ksh/tcsh: Simple newline-separated commands
 * - zsh: Optional timestamp prefix (": timestamp:duration;command")
 * - fish: YAML-like format with "- cmd:" and "  when:" fields
 */

Overall Assessment

This is a production-ready fix that properly handles shell-specific formats. The main suggestion is using synchronous writes to avoid race conditions, and adding debug logging for troubleshooting. The architecture is clean and extensible for future shells.

Recommendation: Approve with minor sync/async consideration. Great refactoring! 🎉

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants