Skip to content

Conversation

@SjB
Copy link
Contributor

@SjB SjB commented Mar 19, 2025

Summary

Add shebang and tangle-mode source block header tags

Related Issues

New features to enable features in org-mode

Related #

https://orgmode.org/manual/Extracting-Source-Code.html

Closes #

Changes

Changes the structure of the tangle_info table.

@kristijanhusak kristijanhusak force-pushed the support_sheband_and_tangle_mode_header_tag branch from 113cc16 to 67667a8 Compare March 25, 2025 19:20
@SjB SjB force-pushed the support_sheband_and_tangle_mode_header_tag branch from 67667a8 to 160fe5f Compare March 29, 2025 14:03
Copy link
Member

@kristijanhusak kristijanhusak left a comment

Choose a reason for hiding this comment

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

Thanks for the PR! Generally looks good, but I have some questions and suggestions.

Comment on lines 83 to 87
local shebang = info.header_args[':shebang']
if shebang then
shebang = shebang:gsub('[\'"]', '')
table.insert(parsed_content, 1, shebang)
end
Copy link
Member

Choose a reason for hiding this comment

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

This logic is already done above on line 64. Is there a reason why it's needed twice?

if mode_str and mode_str:sub(1, 1) == 'o' then
mode_str = mode_str:sub(2)
local mode_num = tonumber(mode_str, 8)
vim.loop.fs_chmod(filename, mode_num)
Copy link
Member

Choose a reason for hiding this comment

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

Instead of doing chmod after the file is created, lets pass down the mode as an argument to utils.writefile and pass it there when creating the file.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

sound good, I'm make the changes

@SjB SjB force-pushed the support_sheband_and_tangle_mode_header_tag branch from e84d8a8 to fdfb3c8 Compare April 16, 2025 04:31
@SjB
Copy link
Contributor Author

SjB commented Apr 16, 2025

The orgmode docs describe 3 styles of tangle-mode values: ls-style, chmod style, and octal. All three style are now supported, however, I'm still using the vim.loop.fs_chmod function, as I can't seem to support all the requirements with the utils.writefile function

example of the three modes

#+BEGIN_SRC text :tangle result-octal.txt :tangle-mode o700
octal mode
#+END_SRC

#+BEGIN_SRC text :tangle result-ls-style.txt :tangle-mode rw-rw----
ls style
#+END_SRC

#+BEGIN_SRC text :tangle result-chmod-style.txt :tangle-mode ug+wx,u+wr,o+x
chmod style
#+END_SRC

@kristijanhusak
Copy link
Member

The orgmode docs describe 3 styles of tangle-mode values: ls-style, chmod style, and octal. All three style are now supported

Nice! 👍🏻

however, I'm still using the vim.loop.fs_chmod function, as I can't seem to support all the requirements with the utils.writefile function

That's fine, I don't think it will cause any issues.

Could you add few tests that test these new tangle options? I'm not sure if CI will cause some issues testing the permissions, but you can add 1 test that modifies the perrmission and push to see how CI behaves.

It's somewhat sensitive stuff so I want to make sure we cover it with tests as much as possible.

SjB added 18 commits January 28, 2026 22:46
Add the ability to include a shebang line at the beginning of tangled code files
by supporting the `:shebang` header argument. The shebang value is cleaned of
quotes and inserted as the first line of the tangled content.
This header allow the creator to change the generated file permissions.
When tangling code blocks to files, automatically create parent directories
when the :mkdirp header argument is set to 'yes'.
Coding style requires single quotes
Add the ability to include a shebang line at the beginning of tangled code files
by supporting the `:shebang` header argument. The shebang value is cleaned of
quotes and inserted as the first line of the tangled content.
They keep showing up.
Add Plenary specs to verify shebang prepending, default 0755 behavior,
and :tangle-mode parsing/override (octal, chmod, ls styles).
@SjB SjB force-pushed the support_sheband_and_tangle_mode_header_tag branch from 6b920eb to 44ac60d Compare January 29, 2026 03:51
Copilot AI review requested due to automatic review settings January 29, 2026 03:51
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds support for :shebang and :tangle-mode source block header tags to enable org-mode babel tangling features. When a shebang is specified, it's prepended to the tangled file and the file permissions default to 0755. The :tangle-mode tag allows explicit file permission control using octal (e.g., "o644"), chmod-style (e.g., "u=rw,go=r"), or ls-style (e.g., "rwxr-x---") formats.

Changes:

  • Added helper functions to convert permission strings (ls-style and chmod-style) to octal format
  • Modified tangle_info structure from a simple array to an object with content and mode fields
  • Implemented shebang prepending with automatic 0755 permission default
  • Added file permission modification using vim.loop.fs_chmod

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 8 comments.

File Description
tests/plenary/babel/tangle_shebang_mode_spec.lua New test file with comprehensive coverage of shebang and tangle-mode features including octal, chmod-style, and ls-style permission formats
lua/orgmode/babel/tangle.lua Core implementation adding permission conversion functions and integrating shebang/mode handling into the tangle workflow

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +17 to +39
function ls_style_to_octal(rwx_string)
local result = 0
local value = 0

for i = 1, #rwx_string, 3 do
local chunk = rwx_string:sub(i, i + 2)
value = 0

if chunk:sub(1, 1) == 'r' then
value = value + 4
end
if chunk:sub(2, 2) == 'w' then
value = value + 2
end
if chunk:sub(3, 3) == 'x' then
value = value + 1
end

result = result * 8 + value
end

return result
end
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

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

These helper functions should be declared as local function instead of global functions. This is consistent with the codebase convention seen throughout the project (e.g., in lua/orgmode/api/agenda.lua, lua/orgmode/colors/colors.lua, and many other files where helper functions are prefixed with local).

Copilot uses AI. Check for mistakes.
Comment on lines +41 to +73
function chmod_style_to_octal(chmod_string)
local owner, group, other = 0, 0, 0

for part in chmod_string:gmatch('[^,]+') do
local who, what = part:match('(%a+)[=+](.+)')
if not who or not what then
return nil
end

local perm = 0
if what:find('r') then
perm = perm + 4
end
if what:find('w') then
perm = perm + 2
end
if what:find('x') then
perm = perm + 1
end

if who:find('u') or who:find('a') then
owner = bit.bor(owner, perm)
end
if who:find('g') or who:find('a') then
group = bit.bor(group, perm)
end
if who:find('o') or who:find('a') then
other = bit.bor(other, perm)
end
end

return owner * 64 + group * 8 + other
end
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

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

This helper function should be declared as local function instead of a global function, following the codebase convention seen throughout the project.

Copilot uses AI. Check for mistakes.
Comment on lines +136 to +140
if info.header_args[':mkdirp'] == 'yes' then
local path = vim.fn.fnamemodify(info.filename, ':h')
vim.fn.mkdir(path, 'p')
end

Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

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

The :mkdirp check is duplicated here. It already appears at lines 117-120, so this duplicate block at lines 136-139 should be removed.

Suggested change
if info.header_args[':mkdirp'] == 'yes' then
local path = vim.fn.fnamemodify(info.filename, ':h')
vim.fn.mkdir(path, 'p')
end

Copilot uses AI. Check for mistakes.
Comment on lines +169 to +172
if mode then
utils.echo_info(('change mode %s mode %o'):format(filename, mode))
vim.loop.fs_chmod(filename, mode)
end
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

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

There is a race condition here. The file mode is set with vim.loop.fs_chmod synchronously after the async utils.writefile is added to the promises array but before Promise.all(promises):wait() completes. This means fs_chmod may execute before the file is fully written, which could fail or set permissions on a partially written file. The fs_chmod call should happen after the write promise is resolved.

Copilot uses AI. Check for mistakes.

if mode then
utils.echo_info(('change mode %s mode %o'):format(filename, mode))
vim.loop.fs_chmod(filename, mode)
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

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

The codebase consistently uses vim.uv for libuv bindings (see lua/orgmode/utils/init.lua:2, lua/orgmode/files/file.lua:46, lua/orgmode/export/init.lua:85, etc.). This should be changed to vim.uv.fs_chmod for consistency.

Copilot uses AI. Check for mistakes.
assert.are.equal(tonumber("0750", 8), stat_mode(tangled_file))
end)

it('should keep 0755 when :shebang is set even if :tangle-mode is also set (mode is overridden if :tangle-mode present)', function()
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

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

The test description says "should keep 0755" but the test actually expects 0700 and verifies that :tangle-mode overrides the shebang default. The description should be corrected to match what the test actually verifies, something like "should apply :tangle-mode even when :shebang is set (mode is overridden if :tangle-mode present)".

Suggested change
it('should keep 0755 when :shebang is set even if :tangle-mode is also set (mode is overridden if :tangle-mode present)', function()
it('should apply :tangle-mode even when :shebang is set (mode is overridden if :tangle-mode present)', function()

Copilot uses AI. Check for mistakes.
end
vim.list_extend(tangle_info[info.filename], parsed_content)
vim.list_extend(tangle_info[info.filename]['content'], parsed_content)
tangle_info[info.filename]['mode'] = filemode
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

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

When multiple blocks tangle to the same file, there's a logic issue with file mode handling. If the first block sets a shebang (which defaults the mode to 0755), and a subsequent block has neither :shebang nor :tangle-mode, the filemode variable will become nil and override the previously set mode on line 145. This means the final file mode will be nil instead of the expected 0755. The mode should only be updated if a new mode is explicitly specified, not reset to nil.

Suggested change
tangle_info[info.filename]['mode'] = filemode
if filemode ~= nil then
tangle_info[info.filename]['mode'] = filemode
end

Copilot uses AI. Check for mistakes.
Comment on lines +17 to +39
function ls_style_to_octal(rwx_string)
local result = 0
local value = 0

for i = 1, #rwx_string, 3 do
local chunk = rwx_string:sub(i, i + 2)
value = 0

if chunk:sub(1, 1) == 'r' then
value = value + 4
end
if chunk:sub(2, 2) == 'w' then
value = value + 2
end
if chunk:sub(3, 3) == 'x' then
value = value + 1
end

result = result * 8 + value
end

return result
end
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

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

The ls_style_to_octal function expects a 9-character string (like "rwxr-xr-x") but doesn't validate the input length. If a malformed ls-style permission string is provided (e.g., "rwx" or "rwxr-xr-x-extra"), the function will either process incomplete chunks or process extra characters incorrectly. Consider adding validation to ensure the string is exactly 9 characters long, or handle variable-length strings appropriately.

Copilot uses AI. Check for mistakes.
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.

2 participants